内容简介:We all care about the performance of web applications that we build. We try to keep production bundles small and loading times low. That's a good thing! It definitely is for the user, who gets a great experience. But is it good for the developer? When we w
We all care about the performance of web applications that we build. We try to keep production bundles small and loading times low. That's a good thing! It definitely is for the user, who gets a great experience. But is it good for the developer? When we want the app to work fast, does it mean that creating it has to be slow? Can we still use external libraries and packages from NPM? Or do we have to write everything from scratch, counting each line of code?
Making a webpage fast may seem like a sacrifice from the developer's point of view. How could you keep a JavaScript bundle below 50 kB when almost any popular library or framework takes half of that budget or even exceeds it? There is a way to find a compromise and keep both performance and easiness given by the ecosystem. Everything we need is the right set of tools.
The story
Let's say we're writing dev.to news listing. For each article fetched from API we're supposed to display a title, beginning of the content, and a list of tags. Tags are returned from the API as a string, using a comma as a separator, so there are some transformations needed to parse and render them.
This code is fairly simple, but it may be even more readable when written using predefined functions, like those form lodash
library. For many developers lodash
is the very first choice when it comes to finding a comprehensive set of useful functions speeding-up the development.
import _ from "lodash/fp"; const renderTags = _.pipe( _.split(","), _.map(_.trim), _.reject(_.isEmpty), _.map(tag => <li className={styles.tag}>{tag}</li>) );
That looks quite neat! But there is a problem - bundle size increased from 12.5 kB to almost 94 kB :scream: Even if code quality could be considered as significantly better, such change would be unacceptable because it simply harms the user.
When we dig into the production bundle in Webpack Stats Explorer , we can see a few modules were added, but there is one that should attract our attention - lodash.min.js
. It takes almost 70 kB, the majority of our bundle!
Click on the screenshot to launch an interactive version of Webpack Stats Explorer
It turns out that by default, no matter how many functions we actually use from lodash
, the whole library is sent down to the user. How to fix it? Named imports are the answer. Instead of importing the whole _
object, we could specify each function we use by name. In the process called "tree shaking", Webpack will extract only the code that we need.
There is some issue with this solution, though. lodash
isn't really tree-shaking-friendly package, so by default switching to named imports changes nothing. To make it work as expected, we have to import each function from separate file.
import { pipe, map } from 'lodash/fp';
becomes
import pipe from 'lodash/fp/pipe'; import map from 'lodash/fp/map';
But this is a huge sacrifice, isn't it? The code doesn't look concise anymore and we start relying on the internal structure of lodash
package instead of public API. Fortunately, instead of altering the code manually, it's enough to add a dedicated Babel plugin - babel-plugin-lodash
and everything just works. We can keep using the named imports syntax.
{ "presets": [ "@babel/preset-env", "babel-preset-preact" ], "plugins": ["babel-plugin-lodash"] }
The plugin does the trick - bundle size goes down by 34 kB. Webpack Stats Explorer shows , that instead of one big file, the bundle contains a lot of small modules. And those are the only ones we actually need.
So the bundle is now 57 kB. Is that good enough? Comparing to 12,5 kB we had before - not necessarily. There is another tool that may help - lodash-webpack-plugin
.
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin'); const WebpackEnhancedStatsPlugin = require('webpack-enhanced-stats-plugin'); module.exports = { plugins: [ new LodashModuleReplacementPlugin({ currying: true }), new WebpackEnhancedStatsPlugin({ filename: 'stats.json' }),
Without any changes to the application code, it shrinks the bundle by another 23 kB. What kind of sorcery is this?! The whole trick is based on substituting some of the internal library functions with simpler alternatives or even no-ops. There are plenty of options available but as our code is fairly simple, we need nothing more than currying.
After all those exertions, we managed to shrink the bundle to 34 kB - that's not bad. But it was 12,5 kB before. Is tripling the bundle size justified by better code readability and extensibility? I doubt! Fortunately, we can do better than that. lodash
isn't the only library containing utility functions available on NPM and definitely not the tiniest one. nanoutils
may be a very decent drop-in replacement. This library helped me a lot in my daily job and I can recommend it to all searching for a utility package that doesn't damage the user experience.
When we simply remove lodash
with all build-time plugins and use raw nanoutils
, the package shrinks by 4 kB. That's already a success, but not so impressive! We can do more than that. Similarly to lodash
, by default nanoutils
isn't tree-shakeable so we can shrink the bundle even more with a Babel plugin .
{ "presets": [ "@babel/preset-env", "babel-preset-preact" ], "plugins": [ ["babel-plugin-transform-imports", { "nanoutils": { "transform": "nanoutils/lib/${member}", "preventFullImport": true } }] ] }
Finally, the bundle has a size no bigger than 13,26 kB. It's only 700 B increase when comparing to the very first version which doesn't use any library. That looks more like a cost we can afford to increase code quality and not feel guilty about breaking user experience.
Conclusions
What lesson does the story tell to us? It is possible to have both performant (at least in terms of bundle size) and elegant code. There are a couple of things I wish you to remember.
Tree-shaking and named imports
Tree-shaking is one of the greatest ideas since the sliced bread, at least in the world of web bundlers. It's supported by Webpack , but also Rollup and Parcel . To take advantage of tree-shaking, you should use named imports in favor of default one. Unless API of the library requires otherwise (ex. because it uses this
under the hood), always write
import { foo } from 'lib'; foo();
instead of
import obj from 'lib'; obj.foo();
Make this syntax your new default.
Build and analytic tools
A vast amount of modern libraries published to NPM is tree-shaking friendly. Unfortunately, for many of them, it isn't enabled by default. Use tools like Webpack Bundle Analyzer and Webpack Stats Explorer to dig deep inside your production bundle and get to know what's exactly in it. If you find modules or pieces of code you suspect you don't need, try to use plugins like babel-plugin-transform-imports to get rid of them.
Drop-in library replacements
For many packages, it's easy to find significantly smaller counterparts with similar functionality and API surface. It's very often the case for utility libraries, but also view frameworks. Think of Preact created to substitute React . To estimate the size of the package before adding it to your project, you can use Bundlephobia . For some libraries, the bottom section provides a shortlist of alternatives, which is also super helpful!
That's it! I hope you enjoyed the article and will have an opportunity to apply the described ideas to real web applications. Feel free to reach me in the comments if you have any questions!
Disclaimer
I am a creator of Webpack Stats Explorer - a free-to-use, open-source tool for developers who care about the performance of their web applications. Recently I also made some minor contributions to nanoutils
.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
在线进制转换器
各进制数互转换器
UNIX 时间戳转换
UNIX 时间戳转换