Meteor supports dynamic imports since release 1.5 (which came out in May 2017) and it has been adopted by all my projects in order to reduce initial bundle size to a minimum.
The package system also allows to bundle package content for dynamic import (it's that just not everybody uses that). However, since a bundled Meteor app in production loads an initial single Javascript file, a proper bundle size is crucial.
This article shows, how to make your packages not only dynamic but also optionally static, based on an environment variable flag.
Let's create a simple package, that contains three stateless UI components:
$ meteor create --package jkuester:uicomponents
$ cd uicomponents
$ mkdir lib && cd lib
$ touch loading.html
$ touch notfound.html
$ touch complete.html
The components themselves are also quite simple (think of a huge library in reality):
<template name="loading">
<span class="uic-loading-icon no-wrap">
<i class="fas fa-fw fa-spin fa-refresh"></i>
<span class="uic-loading-title">{{title}}</span>
</span>
</template>
<template name="notfound">
<span class="uic-notfound-icon no-wrap">
<i class="fas fa-fw fa-ban text-danger"></i>
<span class="uic-notfound-title">{{title}}</span>
</span>
</template>
<template name="complete">
<span class="uic-complete-icon no-wrap">
<i class="fas fa-fw fa-check text-success"></i>
<span class="uic-complete-title">{{title}}</span>
</span>
</template>
In a traditional approach they would all be added in the package.js
file:
Package.onUse(function (api) {
api.versionsFrom('1.9')
api.use('ecmascript')
api.addFiles([
'lib/complete.html',
'lib/loading.html',
'lib/notfound.html',
], 'client')
})
This in consequence makes them available immediately but also adds them all to the bundle, even if you intend to use only parts of them.
The sync style should therefore only be used, when a certain environment flag is passed to the application. Otherwise, the main module should be loaded instead:
Package.onUse(function (api) {
const allowSync = !!(process.env.UICOMPONENTS_SYNC)
if (allowSync) {
api.versionsFrom('1.9')
api.use('ecmascript')
api.addFiles([
'lib/complete.html',
'lib/loading.html',
'lib/notfound.html',
], 'client')
} else {
api.mainModule('uicomponents.js', 'client')
}
})
The main module is where the dynamic import comes into play. It provides a simple API to the outside world that allows to handle the imports:
export const UIComponents = {}
UIComponents.complete = {
template: 'complete',
load: async () => import('./lib/complete.html')
}
UIComponents.loading = {
template: 'loading',
load: async () => import('./lib/loading.html')
}
UIComponents.notfound = {
template: 'notfound',
load: async () => import('./lib/notfound.html')
}
That's it. The only Object imported by default is the UIComponents
Object. All further imports are dynamic, reducing your TTI on the first load dramatically. The Meteor project itself imports these components only in those Templates, that really requires them:
myproject/imports/ui/mytemplate/myTemplate.html
<template name="myTemplate">
{{#if loadComplete}}
{{> complete title="loading complete"}}
{{> loading title="please wait"}}
{{> notfound title="404"}}
{{/if}}
</template>
myproject/imports/ui/mytemplate/myTemplate.js
import { UIComponents } from 'meteor/jkuester:uicomponents'
import { ReactiveVar } from 'meteor/reactive-var'
import 'myTemplate.html'
// this is global Template code and runs only once
// when this template is imported and resolved as module
const uicomponentsLoaded = new ReactiveVar()
Promise.all([
UIComponents.complete.load(),
UIComponents.notfound.load(),
UIComponents.loading.load(),
])
.then(() => uicomponentsLoaded.set(true))
.catch(e => console.error('handle me'))
// ...
Template.helpers({
loadComplete() {
return uicomponentsLoaded.get()
}
})
It's all a little bit simplified but I hope it shows the underlying principle and that little tweaks can have a huge impact. From here you also have many options to go on, like writing a custom loader or extend the main module to a more complex structure. Finally, this is all of course not limited to Blaze but can also be used with any other rendering engine.
If you want a to see a real-world package using this concept, please check out my Meteor Blaze Bootstrap 4 Components package.
Top comments (1)
This is an awesome writeup! Thanks for taking the time to do it.