Code Splitting
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeGo to the login page. If you type a really long username, we yell at you! Yes, we value brevity!
Look at the code behind this: assets/js/login.js
. If the length is longer than 20, we add the warning:
// ... lines 1 - 5 | |
$(document).ready(function() { | |
// ... lines 7 - 12 | |
$('.js-login-field-username').on('keydown', function(e) { | |
// ... lines 14 - 17 | |
if ($usernameInput.val().length >= 20) { | |
const $warning = $('<div class="login-long-username-warning">This is a really long username - are you sure that is right?</div>'); | |
$usernameInput.before($warning); | |
} | |
}); | |
}); |
Code Splitting?
Guess what? Webpack comes with an absolutely killer feature... and I've been keeping it a secret! No more! For this to make sense, I want you to pretend that the code that adds the long username warning is really, really big. I literally mean, imagine the code inside the if
statement is many lines... instead of just two.
In this situation, it's really wasteful to force the user to download all that extra JavaScript... just in case they ever type a long username!
In my perfect world, these two lines - or these many lines, in our imaginary situation - would only be downloaded by the user if and when they type a long username. Yes, I want to lazily load parts of our JavaScript!
With Webpack, this is possible! It's called "code splitting".
Refactoring to a Module
To use code splitting, you first need to move the conditional code to a new module. In the Components/
directory, create a new file called username_validation_error.js
. Export a default function with a $usernameInput
argument:
export default function($usernameInput) { | |
// ... lines 2 - 3 | |
} |
I'll move the two lines from login.js
over to this new function. And I'll change $(this)
to $usernameInput
:
export default function($usernameInput) { | |
const $warning = $('<div class="login-long-username-warning">This is a really long username - are you sure that is right?</div>'); | |
$usernameInput.before($warning); | |
} |
To use the new module, back in login.js
, add import username_validation_error
from ./Components/username_validation_error
:
// ... lines 1 - 4 | |
import username_validation_error from './Components/username_validation_error'; | |
// ... lines 6 - 25 |
And below, just, username_validation_error($(this))
:
// ... lines 1 - 4 | |
import username_validation_error from './Components/username_validation_error'; | |
// ... line 6 | |
$(document).ready(function() { | |
// ... lines 8 - 13 | |
$('.js-login-field-username').on('keydown', function(e) { | |
// ... lines 15 - 18 | |
if ($usernameInput.val().length >= 20) { | |
// ... line 20 | |
username_validation_error($usernameInput); | |
} | |
}); | |
}); |
Let's also log the imported module... which should be a function:
// ... lines 1 - 4 | |
import username_validation_error from './Components/username_validation_error'; | |
// ... line 6 | |
$(document).ready(function() { | |
// ... lines 8 - 13 | |
$('.js-login-field-username').on('keydown', function(e) { | |
// ... lines 15 - 18 | |
if ($usernameInput.val().length >= 20) { | |
console.log(username_validation_error); | |
username_validation_error($usernameInput); | |
} | |
}); | |
}); |
Oh, and make sure you have Components
in your import!
// ... lines 1 - 4 | |
import username_validation_error from './Components/username_validation_error'; | |
// ... lines 6 - 25 |
Over in my Webpack tab, once I finished, Webpack was happy. Refresh the login page and... yep! It still works. Code refactoring complete!
Using the delayed import()
To add code splitting, Webpack has two syntaxes... and both work the same. The first is called require.ensure()
:
require.ensure(['./lazy_module'], function (require) {
const lazyModule = require('./lazy_module');
});
The second - the one I want to show you - uses import
:
import('./lazy_module').then(lazyModule => {
// ...
});
Down in the if
statement, this is the moment when I actually need to load my username_validation_error
module. Add import()
here... but use it like a function. I'll copy the ./Components/username_validation_error
module path, delete that import line entirely:
// ... lines 1 - 4 | |
import username_validation_error from './Components/username_validation_error'; | |
// ... lines 6 - 25 |
And pass that as the first argument. When you use import
like this, it returns a Promise
. Hey, we know about those! It means that we can say .then()
and pass a callback. The argument will be the imported module. So, username_validation_error
:
// ... lines 1 - 5 | |
$(document).ready(function() { | |
// ... lines 7 - 12 | |
$('.js-login-field-username').on('keydown', function(e) { | |
// ... lines 14 - 17 | |
if ($usernameInput.val().length >= 20) { | |
// use code splitting to lazily load this "chunk" | |
import('./Components/username_validation_error').then(username_validation_error => { | |
// ... lines 21 - 22 | |
}); | |
} | |
}); | |
}); |
Move the code inside of the callback:
// ... lines 1 - 5 | |
$(document).ready(function() { | |
// ... lines 7 - 12 | |
$('.js-login-field-username').on('keydown', function(e) { | |
// ... lines 14 - 17 | |
if ($usernameInput.val().length >= 20) { | |
// use code splitting to lazily load this "chunk" | |
import('./Components/username_validation_error').then(username_validation_error => { | |
console.log(username_validation_error); | |
username_validation_error($usernameInput); | |
}); | |
} | |
}); | |
}); |
Yea... I like this! It feels like... and well.. is an AJAX call! We're saying:
Good afternoon Webpack! Could you please download the
username_validation_error
module via AJAX and then execute my callback when it's ready. Thank you!
It should work... but go look at the Webpack terminal. Ah! It's SO angry!
Module build failed. Syntax error:
import
andexport
may only appear at the top level.
The dynamic import Proposal
Hmm. It's very unhappy about the import
: it says that this is only allowed to live at the top of the file!
Here's the story: when you use import
at the top of the file - like we've been doing until now - we're using a real, official ECMAScript feature - it's in ES6. But when you use import()
like a function... well... that's not part of ECMAScript! Well, not yet. That functionality is just a proposal called "dynamic import".
The parse error comes from Babel: it tries to parse our code, but sees this code as invalid. And technically, it's right!
Making Babel like dynamic imports
So here's the plan: we need to teach Babel that this syntax is valid... but not to do anything with it. I mean, it should not have an error, and it should leave the import()
function there so Webpack can parse it.
Doing this is easy: Babel is super configurable. In our open terminal, run:
yarn add babel-plugin-syntax-dynamic-import --dev
This is a plugin for Babel that makes it understand the dynamic import syntax.
Once it's installed, to activate it, open your .babelrc
file. In addition to presets
, the other common thing you'll add here is plugins
. Pass it one: syntax-dynamic-import
:
{ | |
// ... line 2 | |
"plugins": ["syntax-dynamic-import"] | |
} |
Now, Babel will at least understand this as a valid syntax.
Go back to the terminal that's running Webpack and restart it... just to be safe:
yarn watch
Yes! It's happy! Back in the browser, bring up the network tab and refresh. Ok, I'll clear this out. Now, type a really long username.
Woh! Check it out! I don't see the error message... but it did make an AJAX request for a script tag! And look inside! Yea! This is our code-split module!
Using the .default Key
Great! Except... for the fact that it didn't work! Check out the console. Above the errors, remember, we logged the module. But... it's not a function! What!? It's an object... with a key called default
... and that is the function!
This is a gotcha with code splitting. When you export things as default
, the module will actually live on a default
key! No big deal: to get things to work, say username_validation_error.default()
:
// ... lines 1 - 5 | |
$(document).ready(function() { | |
// ... lines 7 - 12 | |
$('.js-login-field-username').on('keydown', function(e) { | |
// ... lines 14 - 17 | |
if ($usernameInput.val().length >= 20) { | |
// use code splitting to lazily load this "chunk" | |
import('./Components/username_validation_error').then(username_validation_error => { | |
username_validation_error.default($usernameInput); | |
}); | |
} | |
}); | |
}); |
Refresh again! Type a long username... and... woohoo! There's our warning! And it was loaded via an AJAX call. Hello code splitting. And though it looks really fast, in a real app, you may want to add a loading animation... like with any other AJAX call.
Code Splitting CSS
And, I have more good news! You can also code split CSS! Open login.css
. At the bottom, yep, this last CSS rule only exists to style the warning box:
// ... lines 1 - 48 | |
.login-long-username-warning { | |
color: #8a6d3b; | |
background-color: #fcf8e3; | |
padding: 15px; | |
margin-bottom: 10px; | |
border: 1px solid #faebcc; | |
border-radius:4px; | |
} |
Just like with our JavaScript, it's wasteful to make the user download this... when they might not need it!
Remove that CSS and create a new file: css/login-username-error.css
. Paste it here:
.login-long-username-warning { | |
color: #8a6d3b; | |
background-color: #fcf8e3; | |
padding: 15px; | |
margin-bottom: 10px; | |
border: 1px solid #faebcc; | |
border-radius:4px; | |
} |
Now, inside username_validation_error.js
, that CSS is really a dependency of this module. So, add import '../../css/login-username-error.css
:
import '../../css/login-username-error.css'; | |
export default function($usernameInput) { | |
// ... lines 4 - 5 | |
} |
When we refresh now, login.css
does not contain that extra code. Yep, we've made that file slightly smaller. But when we try a really long username... it works! Look at the downloaded JavaScript file. Yes! It contains the CSS, down at the bottom.
When the JS file loads, the CSS is being injected onto the page via a traditional style tag. Well, it's a weird blob actually... but conceptually, this is a style
tag with that CSS.
Oh, remember the extract-text-webpack-plugin
?
// ... lines 1 - 3 | |
const ExtractTextPlugin = require('extract-text-webpack-plugin'); | |
// ... lines 5 - 40 | |
const webpackConfig = { | |
// ... lines 42 - 51 | |
module: { | |
rules: [ | |
// ... lines 54 - 63 | |
{ | |
test: /\.css$/, | |
use: ExtractTextPlugin.extract({ | |
use: [ | |
cssLoader | |
], | |
// use this, if CSS isn't extracted | |
fallback: styleLoader | |
}), | |
}, | |
// ... lines 74 - 106 | |
] | |
}, | |
// ... lines 109 - 155 | |
}; | |
// ... lines 157 - 180 |
Well, by default, it does not extract any CSS that has been code split. Nope, any code-split CSS is instead passed to the fallback
loader: style-loader
. In other words, code-split CSS is packaged into JavaScript and added to the page when that JavaScript file is downloaded.
So next time you have some conditional code... think about code splitting: you could drastically reduce the size of your assets!
Speaking of that, let's use a visualizer to make our assets even more efficient.
Thank you so much for this much detail on code splitting. I was learning it and this, along with another detailed article, helped me a lot.