pocket-manual icon indicating copy to clipboard operation
pocket-manual copied to clipboard

关于 Tree Shaking

Open FishPlusOrange opened this issue 7 years ago • 2 comments

对于前端开发来说,tree shaking 这个名词多多少少都有所了解。

tree shaking 是一个术语,通常用于描述移除 JS 上下文中没用的代码,这样可以有效地缩减打包体积。

Webpack 官方文档有一段很形象的描述:

你可以将应用程序想象成一棵树。绿色表示实际用到的源码和 library,是树上活的树叶。灰色表示无用的代码,是秋天树上枯萎的树叶。为了除去死去的树叶,你必须摇动这棵树,使它们落下。

tree shaking 这个概念最早由 ES2015 模块打包工具 rollup 提出。

关于 tree shaking 的实现原理可以阅读一下这篇文章:tree shaking 性能优化实践-原理篇

这里主要从 Webpack 的角度来谈 tree shaking。

Webpack2 增加了 tree shaking 的功能。tree shaking 之所以能够被 Webpack 支持得益于 ES6 modules 静态解析的特性。ES6 的模块声明保证了依赖关系是提前确定的,使得静态分析成为可能。

Webpack 的 tree shaking 大致流程是先通过 importexport 语法找出那些需要删除的无用的代码,再借助工具(如 uglifyjs-webpack-plugin)进行删除。

但是在实际使用过程中 tree shaking 效果并不明显,由于副作用影响,无用的代码并没有完全消除。

副作用的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个 export 或多个 export。举例说明,例如 polyfill,它影响全局作用域,并且通常不提供 export。

关于副作用,可以了解一下这篇文章:你的 tree shaking 并没什么卵用

举一个例子,在 Webpack3 中:

// webpack.config.js
const path = require('path')
const webpack = require('webpack')

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  }
}
// index.js
import { cube } from './math.js'

console.log(cube(6))
// math.js
export function square(x) {
  return x * x
}

export function cube(x) {
  return x * x * x
}

查看输出结果 bundle.js

capture-1546413730

可以看出,由于 index.js 并未从 math.js 模块中 import 导入 square 方法。所以,在 bundle.js 中,square 没有被导入。但是,它仍然被包含在 bundle.js 中。这时候可以通过配置 plugins 启用 UglifyJSPlugin 插件删除无用的代码(也可以通过 webpack -p 命令):

// webpack.config.js
const path = require('path')
const webpack = require('webpack')

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin() // 启用 UglifyJsPlugin
  ]
}

从输出文件 bundle.js 可以看到 square 被删除了(压缩代码就不贴截图了)。

但是,如果是有副作用的代码,即使未被 import 导入,也不会被删除:

// webpack.config.js
const path = require('path')
const webpack = require('webpack')

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  }
}
// index.js
import { cube } from './math.js'

console.log(cube(6))
// math.js
export function square(x) {
  return x.a
}
square({ a: 666 }) // 产生 side effects

export function cube(x) {
  return x * x * x
}

从输出文件 bundle.js 可以看到副作用代码被保留下来了(还是贴一下截图吧 XD):

capture-1546414908

以上基于 webpack v3.6.0

Webpack4 对 tree shaking 进行了扩展优化,可以通过配置 package.json 的配置项 sideEffects,向 compiler 提供提示,表明项目中的哪些文件是纯的 ES2015 模块,由此可以安全地删除文件中未使用的部分。

还是上面的例子,这里简单地将 sideEffects 标记为 false,以告知 Webpack 可以安全地删除未用到的 export 导出:

// package.json
{
  // ...
  "sideEffects": false
  // ...
}

为了删除无用的代码,在 Webpack4 中可以通过设置 modeproduction 来启用 UglifyJSPlugin 插件(也可以通过 webpack -p 命令)。

// webpack.config.js
const path = require('path')

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  mode: 'production' // 启用 UglifyJsPlugin
}
// index.js
import { cube } from './math.js'

console.log(cube(6))
// math.js
export function square(x) {
  return x * x
}

export function cube(x) {
  return x * x * x
}

从输出文件 bundle.js 可以看到副作用代码被删除了(压缩代码就不贴截图了)。

当然,如果代码确实有一些副作用,那么可以将 sideEffects 改为提供一个数组:

// package.json
{
  // ...
  "sideEffects": [
    "./src/math.js"
  ]
  // ...
}

以上基于 webpack v4.28.3 和 webpack-cli v3.1.2

参考:

FishPlusOrange avatar Nov 07 '18 07:11 FishPlusOrange

怎么没有更新

gdx-pooky avatar Nov 25 '18 08:11 gdx-pooky

怎么没有更新

本周整理完更新一波

FishPlusOrange avatar Nov 26 '18 03:11 FishPlusOrange