Replies: 3 comments 4 replies
-
Would this apply to any module types that don't already have render functions? In other words, does this only apply to widget-type and page-type modules? In one example above you used
You're talking about replacing the placeholders with links to the bundles, right? That's not totally clear, though I'm not up on all the advanced tree-splitting (/s) techniques. I'm thinking about the balance of multiple requests versus the single request size if this were to inline instead. Linked resources could be cached, but if each widget has their own bundle then maybe inlining would have been better in some cases. Allowing either could be an option, but of course adds to feature development. |
Beta Was this translation helpful? Give feedback.
-
Hi @boutell, as you know we are in a hurry to see this new bundling capability in action! |
Beta Was this translation helpful? Give feedback.
-
Depending on the actual implementation details, I can see how further optimizations could be implemented like link prefetching to cache large bundles that we expect the user to need, just not on the current page. A use case for that could be a site with one big app that we expect to be part of most user journeys, but we know is not necessarily the entry point for most users. For example if a user lands on page A (e.g. the homepage) that needs only the main bundle, and then navigates to page B (e.g. the search page) that needs That'd be an edge case within the edge case though, so I don't see that making its way to Apostrophe core. In fact now that I think about it, provided we can predict or retrieve the bundle URL, a simple enough user-land implementation would be to add the Well anyway. I'm glad to see this proposal and I'm looking forward to seeing it implemented! (And the Webpack extension capability is a nice touch we might also benefit from on a totally unrelated topic 🙂) |
Beta Was this translation helpful? Give feedback.
-
The following proposal is being reviewed internally for development starting soon in A3.
As you can see it adds support for multiple bundles, so that some of your JavaScript and SCSS can be loaded only on the pages that really need it. This is useful for sites that have a few exceptionally large JavaScript components that aren't needed by most pages.
In addition, it adds support for customing the webpack configuration. This allows you to introduce JavaScript frameworks while still taking advantage of the simplicity of our built-in webpack build and placing your code in
ui/src
of any module.Your input is welcome!
Tech design: multiple UI bundles
Revised 2/20/22
Most projects do well with a single frontend bundle because most JS and CSS code is encountered by most users in the course of their site visit. To separate it into multiple bundles just adds extra requests, so it is inefficient.
However some projects do include JS components that are used only rarely. These components would benefit from the ability to package them in separate bundles which are loaded only in the presence of certain page types, piece types (only when on the show page for that type), or widget types. Enabling that is the purpose of this tech design.
SCOPE RESTRICTIONS
All relevant bundles will be loaded at page load time, alongside the main bundle. While it is possible for custom frontend javascript to load page fragments later, adding new widget types to the display, loading extra bundles on the fly at the time a widget type first is added to the page is not in scope right now. Those anticipating such requirements should activate all of the possible bundles for the page types that have such features.
Only
ui/src
front end JavaScript and SCSS is relevant to this proposal. Admin UI code is not relevant. While there may be admin UI code that is not used on every page, an editor can be reasonably expected to wait an extra quarter second for additional JS code.Matching bundles for styles are also supported for parallelism and consistency.
Deduplication of dependencies is desirable but doesn't have to be perfect. A CMS experience like an Apostrophe site typically has one main bundle and, maybe, one of the extra bundles in play at a time. If two or more extra bundles are in play on a page, that's fine but it's OK to load a few duplicate dependencies in that edge case. However if the main bundle and a custom bundle share npm dependencies these should be shared, since the user must load them anyway as part of the main bundle.
CONFIGURATION
Enabling bundles in a module
Any module can contribute code to one or more custom bundles, and opt its page type templates into those bundles, using the new webpack module section(not an option). Note that we can specify which templates of the module actually load the bundle when rendered as a full page (via Apostrophe's ordinary page rendering or via sendPage), i.e. page.html, show.html, index.html. If we don’t specify which templates should be opted in with the templates array property, or opt out with an empty templates property, the default is any full-page template rendered by the module.
Widget modules are a special case: any use of that widget on a page opts the page into loading the bundles configured by that module.
Code entry points for custom bundles
The following filename convention is used to incorporate code into the main bundle (this part has not changed) and into a custom bundle called dealer (this part is new):
Matching scss entry points also work in bundle folders, as would be expected.
dealer.js
has no special significance unless this specific module folder'sindex.js
configures the dealer bundle. For any other module, including a subclass of one that uses the bundle,dealer.js
is just an ordinary file thatindex.js
might choose to import. This is done to ensure that creating an "improvement" to pieces that happens to include an asset bundle doesn't suddenly change the meaning of unrelated.js
files inui/src
in all piece types.Extending Webpack
Any module can also extend the Webpack configuration by adding an extensions subproperty to the webpack section:
Each subproperty of extensions is merged with the webpack configuration for
ui/src
builds usingwebpack-merge
which is already used in Apostrophe's admin UI build.The sub-objects, like v
ue2
, exist for:code structure, it's good namespacing: "this is what I'm trying to add support for here"
To permit explicit overrides of each by name, last module wins
To prevent conflicts: if two npm modules both need Vue 2 and aren't sure if the other is present, it's OK because they can both ship a vue2 property, and only the last one is used so we don't really wind up with two instances of
vue-loader
being active in the build at the same timeOf course it would be a good practice for apostrophe npm modules that ship with webpack build extensions for apostrophe to require the details above from a little npm module that other apostrophe npm modules can use too, for more consistent results no matter who "wins" and gets to configure
vue2
. At some point in the future we could also configure offering "presets" for common cases, if we choose to take on that task.IMPLEMENTATION ISSUES
The discovery problem
In order to discover all of the required bundles for widgets, code will be added to the
renderWidget
method of the@apostrophecms/area
module and the widget custom tag (widget.js
) just before they invoke manager.output. This code will check a list of bundles active in the widget module or any of its parent classes and add any bundles listed there toreq.aposBundles
.In order to discover the required bundles for the current page and/or piece, the
sendPage
method of@apostrophecms/module
will check the module name and template name to see if any bundle is configured for them by the module or its parent classes (taking into account thetemplates
setting if any), and add any bundles listed there toreq.aposBundles
.Implementing the loading of extra bundles: the timing problem
Currently the stylesheetsHelper and scriptsHelper methods of the
@apostrophecms/asset
module are straightforward. They output one tag each into the markup. However because areas, widgets and async components are all asynchronous and can appear anywhere on the page, at the time these script and stylesheet helpers are called it is not possible to know what widgets might appear somewhere in the page output. This is especially true for the stylesheets helper but in the end it is true for both because we cannot force Nunjucks to execute the code in a specific order.So
stylesheetsHelper
andscriptsHelper
will be replaced with empty functions for backwards compatibility (because people might extend them), comments will be added toouterLayoutBase
indicating that these calls are just for bc, and the@apostrophecms/asset
module will instead inject async components:These new async components, which have access to req, will generate random identifiers, record them as
req.stylesheetsPlaceholder
andreq.scriptsPlaceholder
, and output them into the page.The
renderPageForModule
method of@apostrophecms/template
will then be updated to replace these placeholders with the actual markup for all of the bundles applicable to the current page just before content is returned. If the placeholders do not appear in the string the function is about to return, or the placeholder properties of req are not set, then nothing happens.The dependency duplication problem
If we just build multiple bundles naively we'll get duplication of code between those bundles when they require the same dependencies from npm, which adds download time for the end user.
As mentioned before a perfect solution to this is not in scope, however a simple solution is provided: our webpack build will define an entry point for each bundle, and use the
dependOn
setting, so that any dependencies of the main bundle are automatically reused if needed by the extra bundles.FREQUENTLY ASKED QUESTIONS
What if one module configures vue2 and the other vue3 as webpack extensions, but they both want to load all .vue components found in ui/src? Won't there be conflict?
There absolutely will be conflict, but it's not something we can fix by ourselves. We can only document the possible issues.
vue-loader
is not currently designed to support both Vue2 and Vue3 in the same project at all, not even in separate webpack builds. Down the road we might have to write a vue2-loader that at least stays out of that fight so our admin UI doesn't preclude use of Vue3 at project level. But that is not a new problem introduced by this new feature.So for now best practice is to stick to vanilla modern JS if you're writing an npm module that uses ui/src and is intended for maximum compatibility with every Apostrophe site out there, or to be up front about what your compatibility issues are when releasing it.
At the moment only vue2 would actually work, because of the
vue-loader
bug mentioned above, but again that's not new and not related to this proposal really.What about having a "vendor bundle?"
Apostrophe ALWAYS loads the main bundle, therefore the main bundle IS the vendor bundle. Anything it depends on will always be loaded, so we will generate a
dependOn
clause in the webpack build for the other bundles to take advantage of that and save load time.Yes, there are situations where the "extra" bundles share some dependencies that the main bundle does not need, but it is relatively rare for two or more "extra" bundles to be on the page at the same time. If you are absolutely on fire for them to share dependencies, you can import those dependencies to the main bundle simply to make that happen. But by the time that happens the odds are you are getting close to a situation where you should have had just one main bundle in the first place.
If we turn out to be wrong about this in practice for a significant number of cases, we can always add more support for an explicitly shared "vendor bundle" not loaded by the main bundle at a later time, but we don't want to add engineering time and complexity for an unproven benefit.
What about sharing dependencies with the admin UI bundle?
We just don't need it. Editors will not object to waiting an extra quarter second. This type of intense performance optimization is for the general public's build.
What about dynamic loading of bundles?
Dynamic loading isn't in scope right now. The main use would be to accommodate a bundle that isn't needed unless the user decides to take an action that doesn't involve navigating to a new page. That is relatively unusual in a CMS project. A second use is a bundle that becomes needed when a "load more" button is clicked to load more content into an index template. The best solution for that is to activate all the bundles that might be needed for the index template of the appropriate piece page module.
Beta Was this translation helpful? Give feedback.
All reactions