DEV Community

loading...

Lightweight Meteor Packages with Conditional Dynamic Imports

Jan Küster
Graduated in Digital Media M.Sc. now developing the next generation of educational software. Since a while I develop full stack in Javascript using Meteor. Love fitness and Muay Thai after work.
・3 min read

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<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>  
Enter fullscreen mode Exit fullscreen mode
<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>  
Enter fullscreen mode Exit fullscreen mode

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')  
})
Enter fullscreen mode Exit fullscreen mode

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')  
  }  
})
Enter fullscreen mode Exit fullscreen mode

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')  
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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()
  }
})
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (1)

Collapse
copleykj profile image
Kelly Copley

This is an awesome writeup! Thanks for taking the time to do it.