We reduced our vendor.js from 210kb to 16kb in about five minutes of work and ten lines of code

Ben Halpern on December 20, 2018

Even though we strive for a minimal JavaScript load on dev.to, we had gotten lazy with our optimization. Our vendor.js file, which includes all the... [Read Full]
markdown guide
 

Hat tip to @nickytonline for first introducing this concept into the DEV codebase a while ago.

Thanks for a great post going over this stuff @goenning .

We're all making @addyosmani proud. 😄

 

Great stuff! If you keep looking, you'll find many more places you can apply this!

Chrome Code Coverage is also a great tool to find downloaded javascript/css that has not been used.

 

Wasn't searching for the 🎩 tip, but I'll take it! It's still crazy how little effort it took to get that perf gain. 💪💯

 

So proud! :') Great work, Ben and team! These kinds of vendor bundle savings are awesome to see. Also, so good all of these are open-source for others to learn from.

Might be worth considering bundlesize for CI/PR JS budgets at some point :)

 

Noice! For those interested, here's another PR in the codebase that uses a dynamic import... because perf!

github.com/thepracticaldev/dev.to/...

It's a good use of a dynamic import in this case because we only want to load the on-boarding flow if the user has never seen it.

 

Great job Ben =)

Out of curiosity, where are the other 194KB? I would assume they are in another vendor chunk. Right?

Webpack 4 has the concept of initial and async chunks. Dynamic imports are automatically separated from your original bundle.js and are downloaded on demand by the Webpack runtime.

An interesting thing is that you can prefetch your async bundle. Prefetching is an ambiguous term but it means that the browser will download this resource with super low priority. This is cool because you can induce the browser to not block your render but download it as soon as possible in such a way that it will possibly be already there by the time it is needed. I didn't check the internals of how this works in Webpack 4 but there is a high change that the Webpack runtime will auto prefetch async bundles.

 

The other 194 are in chunks that load when import is called within the code.

Some are quite deep in app logic and we really never want them for most visits. They are only called as necessary. We would maybe want to prefetch them once folks get close to where they would be hidden, but that's about it.

 

Unfortunately, Firefox has not yet implemented dynamic import. How do you do progressive enhancement for module browsers that don't implement import()?

 

TLDR; : with transpilation

dev.to uses webpacker which supports dynamic import (it also supports prefetch and preload). This in turn is supported through Babel (a transpiler), with @babel/plugin-syntax-dynamic-import

 

well, in order to transpile import() you need a way to a) convert modules to scripts and b) dynamically load those scripts.

Sounds like Async Module Definition.

I did a half-hearted poke through the production code, and it looked more like cjs to me. Just curious what kind of 'modules' are actually serving down to clients.

 
 

Yes, looks like it

  (async () => {
    const moduleSpecifier = './utils.mjs';
    const module = await import(moduleSpecifier)
    module.default();
    // → logs 'Hi from the default export!'
    module.doStuff();
    // → logs 'Doing stuff…'
  })();
 

I presume you could do something like this?

const moduleImport = async (loc, callback) => {
  const module = await import(loc);
  callback(module);
}

moduleImport("./dog", ({ bark }) => { bark("Hello World") });

How is this better than just using a promise, as shown in the post?

 

you could even

  (async () => {
    const moduleSpecifier = './utils.mjs';
    const { default: utils, doStuff } = await import(moduleSpecifier)
    utils();
    // → logs 'Hi from the default export!'
    doStuff();
    // → logs 'Doing stuff…'
  })();

Which is similar to the static syntax

import { default as utils, doStuff } from './utils.js';
 

You can make a second step, and load it when its needed (or not load when its not needed - i assume not every page needs it).

Simplified example for prism.js:

if ($q('code[class*="language-"]')) {
  import(/* webpackChunkName: "syntaxHighlighting" */ './js/syntaxHighlighting');
}

Notes:

  • chunkName makes it prettier than 0.fade4.js :)
  • $q - is an alias for document.querySelector
  • file is not exporting anything, just initializing, so there is no need to run .then()... ;)
  • if you have something that you want to prefetch/preload because its also an option: webpack.js.org/guides/code-splitti...
  • link above has also some links to help you understand webpack bundle and make better informed decisions: webpack.js.org/guides/code-splitti...
  • you can construct path to file to be imported dynamically, so you can have a function (simplified, naive, just for demo purposes):
const dynamicImport = p => import(`modules/${p}`).then(m => m.default())

And then call it from your js, when something happens.

If dev.to wasnt so heavy into react i could do some good with my webpack knowledge, but i dont want to get dirty with all the abstractions in there ;)

 

Good job Ben! Hats off to you and @goenning for showing this technique.

 
 

This is very nice ! I didn't know Dynamic import at all, and I'll definitely try it out !

 
 
 

Ugh this is so good—good job on keeping things light!

 
 

I think this post by @quii is relevant to this discussion. I hope we can do a lot more to improve on this front.

 

This is cool, I haven't seen this technique before

Is there a chance it could make some features a bit slow when they're first used?

By doing this, a few seldom-used libraries will only get called when the user triggers an action in our code.

So if i click some button, it's now that it downloads, parses and executes the JS; which might be slow.

I guess it's all trade offs and from my point of view it seems like a good one.

 

So if i click some button, it's now that it downloads, parses and executes the JS; which might be slow.

Prefetching would be the next step but it requires more than 5 minutes:

The other 194 are in chunks that load when import is called within the code.

Some are quite deep in app logic and we really never want them for most visits. They are only called as necessary. We would maybe want to prefetch them once folks get close to where they would be hidden, but that's about it.

I guess it's all trade offs and from my point of view it seems like a good one.

Yeah, you still download it only one time

code of conduct - report abuse