Code Splitting
Introduction
Code splitting is a technique used to reduce the size of your bundle by splitting your code into various bundles which can then be loaded on demand or in parallel. This results in smaller bundles which leads to faster load time. Flarum instances can have a lot of extensions installed, and when each extension lazy loads the modules it does not immediately or frequently need, the initial load time of the forum can be significantly reduced. The opposite leads to a bloated bundle and a slow initial load time.
How to Split Your Code
If you wish to split (lazy load) a module, you can use the asynchronous import() function. This function returns a promise which resolves to the module you are importing. Webpack will automatically split the module into a separate chunk file which will be loaded on demand.
import('acme/forum/components/CustomPage').then(({ default: CustomPage }) => {
// do something with CustomPage
});
This will create a chunk file under js/dist/forum/components/CustomPage.js. This chunk file will be loaded when the import is called. But before that can happen, the backend needs to be made aware of this chunk file. You do that by adding the js/dist/forum path as a source for the forum frontend. (If the chunk was under js/dist/admin, you would add it as a source for the admin frontend, same for js/dist/common and common.)
In extend.php:
use Flarum\Extend;
return [
(new Extend\Frontend('forum'))
->jsDirectory(__DIR__.'/js/dist/forum'),
];
Importing split modules from core or other extensions
Flarum by default lazy loads certain modules of its own, such as the LogInModal component. If you need to import one of these modules, you can do so by just asynchronously importing it as you would any other module.
import('flarum/forum/components/LogInModal').then(({ default: LogInModal }) => {
// do something with LogInModal
});
For modules from other extensions, you can import them using the ext: syntax.
import('ext:flarum/tags/common/components/TagSelectionModal').then(({ default: TagSelectionModal }) => {
// do something with CustomPage
});
Extending/Overriding/Adding split modules methods
If you wish to extend, override or add a method to a split module, rather than directly accessing the module prototype Component.prototype or passing the prototype to extend or override, you have to pass the import path as a first argument to either extend or override utilities. The callback will be executed when the module is loaded. Checkout Changing The UI Part 3 for more details.
Code APIs that support lazy loading
The following code APIs support lazy loading:
Async Modals
You can pass a callback that returns a promise to app.modal.show. The modal will be shown when the promise resolves.
app.modal.show(() => import('flarum/forum/components/LogInModal'));
Async Pages
You can pass a callback that returns a promise when declaring the page component.
import Extend from 'flarum/common/extenders';
export default [
new Extend.Routes()
.add('acme', '/acme', () => import('./components/CustomPage')),
];
Async Composers
If you are using a custom composer like the DiscussionComposer, you can pass a callback that returns a promise to the composer method.
app.composer.load(() => import('flarum/forum/components/DiscussionComposer'), { user: app.session.user }).then(() => app.composer.show());
Flarum Lazy Loaded Modules
You can see a list of all the modules that are lazy loaded by Flarum in the GitHub repository.
Prefetching split chunks
Code splitting keeps the initial bundle small, but it moves the cost of loading a chunk to the first time its route or feature is used — the browser must fetch the chunk over the network before the component can render. For a page the user is very likely to visit (for example, the post stream when they open a discussion), that first-visit request adds a noticeable delay.
To avoid this, you can register a chunk to be prefetched in the background once the app has finished booting and the browser is idle. By the time the user navigates to it, the import() resolves from cache instead of blocking on a request.
Register a loader with app.prefetch:
app.prefetch.add('acme.customPage', () => import('./components/CustomPage'));
- The key (
'acme.customPage') is a unique name for your prefetch, following the usualItemListconventions, so other extensions can reorder or remove it. - The value is the same kind of loader you would pass to a lazy route — a function returning a dynamic import.
- An optional third argument sets the priority; higher priorities are prefetched first.
Loaders run one at a time, each waiting for the next idle period, so prefetching never competes with rendering or user interaction. A failed prefetch is ignored silently — it is only an optimisation, and the real import() on navigation will surface any genuine load error.
Only prefetch chunks the user is likely to need soon. Prefetching everything would defeat the point of code splitting by pulling the whole app over the network up front.
Extending a split component class
Often, you may want to create a component that extends a split component class. Here is a common example, the fof/byobu extension has a PrivateDiscussionComposer component which extends flarum/forum/components/DiscussionComposer.
The DiscussionComposer along with other modules related to the composer, are lazy loaded. So this line of code will not work:
import PrivateDiscussionComposer from './discussions/PrivateDiscussionComposer';
app.composer.load(PrivateDiscussionComposer, {
user: app.session.user,
recipients: recipients,
recipientUsers: recipients,
});
app.composer.show();
Because flarum/forum/components/DiscussionComposer is not loaded yet, the frontend will throw an error, complaining of not being able to find that module.
What we need to do in this case, is to first ensure that flarum/forum/components/DiscussionComposer has been loaded, then we can load the custom component, that means we have to lazy load the custom component:
const PrivateDiscussionComposer = await app.composer
.load(() => import('flarum/forum/components/DiscussionComposer').then(async () => {
return await import('./discussions/PrivateDiscussionComposer');
}), {
user: app.session.user,
recipients: recipients,
recipientUsers: recipients,
});
app.composer.show();