Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
May 28, 2021 08:24 pm GMT

Minimizing Webpack bundle size

The dreaded loading spinner

The two key metrics in determining whether users will stay on your site is the time it takes to load the page and the time it takes to interact with it. The first is First Contentful Paint and the second is Time to Interactive. You can find these metrics for your own site by going to your developer tools and generating a report under the Lighthouse tab on Chrome.

Lighthouse metrics for our web app
Lighthouse metrics for a random web app

By minimizing the size of the bundle, we reduce the time it takes for browsers to download the JavaScript for our site, improving user experience. With every additional second of wait time, the user is more likely to close the tab. Consider all of the users that visit your site everyday and that can be thousands of seconds wasted. The chance of losing a potential user is even higher when you have a complex web app, making it even more important to ensure the bundle size stays low.

Understanding the situation

Lets start by getting an understanding of all the code & dependencies that need to be sent to the browser, along with the memory size of each. Adding webpack-bundle-analyzer to your webpack configuration is the perfect starting point.

Install:

yarn add -D webpack-bundle-analyzer# ornpm install --save-dev webpack-bundle-analyzer

Usage:

import WebpackBundleAnalyzer from 'webpack-bundle-analyzer'webpackConfig.plugins = [  new WebpackBundleAnalyzer.BundleAnalyzerPlugin(),]

After compiling your bundle, your browser should open up a visualization of all the content and its memory sizes:

Visualization of the bundle
Visualization of the bundle

Tree shaking

Webpack works by building a dependency graph of every module imported into our web app, traversing through files containing the code we need, and bundling them together into a single file. As our app grows in complexity with more routes, components, and dependencies, so does our bundle. When our bundle size exceeds several MBs, performance issues will arise. Its time to consider tree shaking as a solution.

Tree shaking is a practice of eliminating dead code, or code that weve imported but do not utilize. Dead code can vary from React components, helper functions, duplicate code, or svg files. Let's go through ways of reducing the amount of dead code we have with help from some Webpack plugins.

babel-plugin-import

The babel-plugin-import plugin for babel-loader enables Webpack to only include the code we need when traversing through dependencies during compilation, instead of including the entire module. This is especially useful for heavy packages like antd and lodash. More often than not, web apps only need select UI components and helper functions, so lets just import whats needed.

Install:

yarn add -D babel-plugin-import# ornpm install --save-dev babel-plugin-import

Usage:

webpackConfig.module.rules = [  {    test: /\.(js|jsx)$/,    include: [path.resolve(__dirname, 'src', 'client')],    use: [{      loader: 'babel-loader',      options: {        plugins: [          // modularly import the JS and styles that we use from antd          [            'import',            { libraryName: 'antd', style: true },            'antd',          ],          // modularly import the JS that we use from @ant-design/icons          [            'import',            {              libraryName: '@ant-design/icons',              libraryDirectory: 'es/icons',            },            'antd-icons',          ],        ],      },    }],  },]

We instantiated two instances of babel-plugin-import, one for the antd package and the other for the @ant-design package. Whenever Webpack encounters import statements from those packages, it is now selective in terms of what part of the package to include in the bundle.

import { Dropdown } from 'antd'// transforms tovar _dropdown = require('antd/lib/dropdown')

babel-plugin-lodash

Similar to babel-plugin-import, the babel-plugin-lodash plugin cherry picks the code we need to import from lodash. The parsed size of the entire lodash package is ~600KB, so we definitely dont want everything.

Install:

yarn add -D babel-plugin-lodash# ornpm install --save-dev babel-plugin-lodash

Usage:

webpackConfig.module.rules = [  {    test: /\.(js|jsx)$/,    include: [path.resolve(__dirname, 'src', 'client')],    use: [{      loader: 'babel-loader',      options: {        plugins: [          ...,          // modularly import the JS that we use from lodash          'lodash',        ],        presets: [          ['@babel/env', { targets: { node: 6 } }],        ],      },    }],  },]

If youre already using babel-plugin-import for lodash, this may be unnecessary, but its always nice to have alternatives.

import _ from 'lodash'const objSize = _.size({ a: 1, b: 2, c: 3 })// transforms toimport _size from 'lodash/size'const objSize = _size({ a: 1, b: 2, c: 3 })

context-replacement-plugin

Looking at the visual of bundle.js, the locale data in the moment package already makes up 480KB. In the case that no locale functionality is used, we should remove that portion of the package from the bundle. Webpacks ContextReplacementPlugin is the best way to do this.

670KB total
670KB total

import webpack from 'webpack'// only include files matching `/(en)$/` in the `moment/locale` contextwebpackConfig.plugins.push(  new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /(en)$/),)

A quick look at the bundle analyzer visualization shows that this simple plugin already shaves ~480KB off our bundle size. A very quick win.

176KB total
176KB total

moment-timezone-data-webpack-plugin

If youre using moment-timezone in your app, youll find moment-timezone-data-webpack-plugin extremely useful. Moment-timezone includes a comprehensive json file of all timezones for a wide date range, which results in a package size of ~208KB. As with locales, its highly likely we dont need this large data set, so lets get rid of it. This plugin helps us do that by customizing the data we want to include and stripping out the rest.

Install:

yarn add -D moment-timezone-data-webpack-plugin# ornpm install --save-dev moment-timezone-data-webpack-plugin

Usage:

import MomentTimezoneDataPlugin from 'moment-timezone-data-webpack-plugin'// only include timezone data starting from year 1950 to 2100 in AmericawebpackConfig.plugins.push(  new MomentTimezoneDataPlugin({    startYear: 1950,    endYear: 2100,    matchZones: /^America\//,  }),)

A before and after analysis shows the package size shrinking to 19KB from 208KB.

Code splitting

A major feature of Webpack is code splitting, which is partitioning your code into separate bundles to be loaded on demand or in parallel. There are a couple ways code splitting can be done through Webpack, one of which is having multiple entry points and another is having dynamic imports. Well be focusing on dynamic imports.

Polyfills

A fitting use case for code splitting is polyfills, since they're only neccessary depending on the browser. We don't know in advance whether a polyfill would be required until the client fetches the bundle, and thus we introduce dynamic imports.

In cases where a dependency is used for something that is already supported by some browsers, it may be a good idea to drop the dependency, use the native function supported by most browsers, and polyfill the function for browsers that dont support it. One example is getting the timezone.

import moment from 'moment-timezone'moment.tz.guess()// works the same asIntl.DateTimeFormat().resolvedOptions().timeZone

If we get Intl.DateTimeFormat().resolvedOptions().timeZone polyfilled on the older browsers, we can completely drop moment-timezone as a dependency, reducing our bundle size by an extra ~20KB.

Lets start by adding the polyfill as a dependency.

yarn add date-time-format-timezone# ornpm install date-time-format-timezone

We should only import it if the browser does not support it.

if (!Intl.DateTimeFormat().resolvedOptions().timeZone) {  import(/* webpackChunkName: polyfill-timezone */ date-time-format-timezone).then((module) => module.default)}

As Webpack traverses through the code during compilation, itll detect any dynamic imports and separate the code into its own chunk. Weve accomplished two things: reducing the size of the main bundle, and only sending the polyfill chunk when necessary.

Frontend routes

For complex web apps that can be divided into sections, route-based code splitting is a clear solution. For example, a website may have an 'e-commerce' section and an 'about the company' section. Many users who visit the site only interact with the e-commerce pages, so loading the other sections of the web app is unnecessary. Lets reduce our bundle size by splitting our main bundle into many bundles to be loaded on demand.

If youre using React, good news because route-based code splitting is pretty intuitive in this framework. Like with the example shown earlier, dynamic imports is used to partition the app into separate bundles.

import React, { Suspense, lazy } from 'react'import { BrowserRouter, Route, Switch } from 'react-router-dom'import LoadingScreen from 'components/LoadingScreen'const App = (props) => (  <BrowserRouter>    <Suspense fallback={<LoadingScreen />}>      <Switch>        <Route exact path="/" component={lazy(() => import('routes/landing'))} />        <Route path="/shop" component={lazy(() => import('routes/shop'))} />        <Route path="/about" component={lazy(() => import('routes/about'))} />      </Switch>    </Suspense>  </BrowserRouter>)

Once we have this code in place, Webpack will take care of the bundle-splitting.

Removing duplicate dependencies

Duplicate dependencies arise when dependencies with overlapping version ranges exist. This generally happens due to the deterministic nature of yarn add and npm install. As more dependencies are added, the more likely duplicate packages are installed. This leads to an unnecessarily bloated size of your web app and bundle.

Fortunately, there are tools for this. If youre using yarn version 2 or greater, you can skip this as yarn has taken care of it automatically. These tools work by moving dependencies with overlapping version ranges further up the dependency tree, enabling them to be shared by multiple dependent packages, and removing any redundancies.

If youre using yarn 1.x:

yarn global add yarn-deduplicateyarn-deduplicate yarn.lock

Or if you use NPM:

npm dedupe

Upgrading and removing dependencies

Look at the bundle visual again and check if the large dependencies support tree shaking and whether there is a similar but smaller package that does everything you need. Upgrading dependencies frequently is recommended, as package size usually slims down over time and as tree shaking is introduced.

Lastly, production mode

Make sure Webpack is in production mode on release! Webpack applies a number of optimizations to your bundle, including minification with TerserWebpackPlugin if youre using Webpack v4 or above. If not, youll have to install and add it manually. Other optimizations include omitting development-only code and using optimized assets.

Summary

Weve covered the importance of bundle size, analyzing the composition of a bundle, tree shaking, code splitting, dependency deduplication, and various Webpack plugins to make our lives easier. We also looked into dynamic imports and loading code on demand. With these practices introduced into your webpack.config.js file, you can worry less about those dreaded loading spinners!

Weve applied these practices to our code at Anvil, and believe sharing our experience helps everyone in creating awesome products. If youre developing something cool with PDFs or paperwork automation, let us know at [email protected]. Wed love to hear from you.


Original Link: https://dev.to/useanvil/minimizing-webpack-bundle-size-38gg

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To