avatar

Coderek's blog

Loneliness is the gift of life

Frontend bundle optimization

Tree shaking

Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on the static structure of ES2015 module syntax, i.e. import and export. The name and concept have been popularized by the ES2015 module bundler rollup.

Tree shaking only works well with ES modules, because there is no dynamism in the import and export. Its dependency can be statically analysed, hence obtaining the unreachable module/code that can be removed during optimization step. Tree shaking is usually only concerning client side development.

Example

When importing lodash.js, it will import the entire module (~500kb) if we do the following.

import { merge } from 'lodash'

This is because lodash package's index file is lodash.js, which contains commonjs build of entire lodash library. However, lodash has also included individually built function files in the package. For example, node_modules/lodash/merge.js. So we can include it like this.

import merge from 'lodash/merge'

We can use babel transform-imports plugin to change the previous form to the latter one. Another better way is to use the ES version of lodash called lodash-es. Lodash-es is a custom build of lodash which has all file written in ES module. Thus it's very easy to do tree shaking.

import { merge } from 'lodash-es'

Webpack Support (v4 and v5)

https://v4.webpack.js.org/guides/tree-shaking/ usedExports optimization config If a module's export is used then it will be included in the final code. If a module's export is not used it may be excluded in the final code. In the latter case, webpack has to recursively walk through the dependency of imported module to make sure there is no side effect.

In computer science, an operation, function or expression is said to have a side effect if it modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the intended effect) to the invoker of the operation.

Example:

// console.patch.ts
const log = window.console.log
window.console.log = function (msg) {
  log('patched log', msg)
}


// helper.ts
import 'console.patch.ts'
export function mul(a, b) {
    return a * b
}

Another example is when you import an CSS file. sideEffects property of package.json sideEffects property give hints to webpack that when checking the import statement of a package, it could skip some of the modules. This property is there to complement the usedExport prop to achieve more efficient dead code detection. Example:

// api.ts
import { backend  } from '@awesome-library' // sideEffects: false 

export function getFilterConfigs() {
    return backend.getFilterConfigs()
}

let init = false
export function initAPI() {
    console.log('intializing API')
    init = true
}

// index.ts
import { initAPI } from 'api.ts'

initAPI()

Code splitting

The purpose is to create runnable chunks of code that can be loaded on demand from frontend. This will allow frontend to load minimum amount of codes for the app to perform well.

In HTTP1.1, browser usually limits the number of concurrent request to server. If there are more chunks needed for a page than the browser allows, the loading will block the rendering of the page until all needed chunks are loaded. So it's advisable to have at most a couple of chunks per page.

In HTTP2.0 however, the browser is able to fetch unlimited number of concurrent request from server. There isn't a limit on the number of chunks. In an ideal case, where Javascript module has become ubiquitous, each module can be its own chunk.

However in real life cases, individual request to fetch data in HTTP2.0 still costs some overhead at protocol level. Thus, it's still good to bundle common codes together and fetch and evaluate at once.

Webpack & HTTP/2 https://medium.com/webpack/webpack-http-2-7083ec3f3ce6

SplitChunks

In webpack 4 & 5, splitChunks was added to replace the old way to split chunks.

Webpack 4: Code Splitting, chunk graph and the splitChunks optimization https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366 Webpack automatically split your code into different chunks based on a few heuristics.

  • New chunks can be shared OR modules are from the node_modules folder
  • New chunk would be bigger than 30kb (before min+gz)
  • Maximum number of parallel requests when loading chunks on demand would be lower or equal to 5
  • Maximum number of parallel requests at initial page load would be lower or equal to 3

Note that if there are duplicated modules in the project dependencies, bundler will consider them as separate modules.

The heuristics are fully tunable. You may refer to the documentation for finer controls. One useful quick setting is to turn split chunks on for both initial and async chunks. Async chunks are generally those imported using dynamic import statement. Initial chunks are those synchronsely loaded from entry points. Turning on split chunks for both enables more code sharing within the given heuristics.

cacheGroup

If you want to enable heuristics per module name/regex, you can also opt to use cacheGroup. cacheGroup also allows you to set priority, so a module only goes to the bundle with higher priority.

Recommendations

  1. Try to reduce the number of duplicated packages. "yarn why xxx"
  2. When building a library, always:
  • Use sideEffects to mark files with side effects
  • Build with ES module version by default and commonjs module for compatibility
  • Use index files with care. Do not export all the components or function at top level, it's better to namespace them according to main function.
  1. If a bundle is too big compared to others try to break it into smaller ones so they can be loaded in parallel
  2. Always prefer to import ES version of modules rather than commonjs version
  3. Avoid exporting using "default"
  4. If a library is not set up properly with sideEffects hints, try to load the ES modules from sub folder index file. e.g. lodash/merge
  5. Use transform-import babel plugin if needed (but remember to disable it for tests)
(End of article)
Hello {{user.name}} ({{user.email}})

Comments ({{comments.length}})

Comments


From {{comment.name}} {{comment.published_at}}
{{comment.body}}
No comment yet