前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >万字梳理 Webpack 常用配置和优化方案

万字梳理 Webpack 常用配置和优化方案

作者头像
Chor
发布2021-09-08 11:07:08
2.3K0
发布2021-09-08 11:07:08
举报
文章被收录于专栏:前端之旅前端之旅
以下是本文的思维导图:

环境: webpack v4.46.0,Nodejs v16.6.2

安装

新建项目文件夹并安装 webpack 和 webpack-cli:

在项目根目录下新建 webpack.config.js,作为 webpack 的默认配置文件。

核心概念

module、chunk 和 bundle

用一张图来方便理解:

简单地说,module 是任何通过 import 或者 require 导入的代码,包括 js、css、图片资源等;多个 module 可以组成一个 chunk,每个 chunk 经过处理后会输出一个 bundle 文件

entry 和 output

1)单入口单出口

以 src 目录下的 index.js 作为入口文件,打包输出文件 bundle.js 到 dist 目录下

这种配置一般用于单页应用,最终只会产生一个 chunk。

2)多入口单出口

对应的 entry 改用数组:

这种配置一般用于多页应用,但最终也只会产生一个 chunk。

3)多入口多出口

对应的 enrty 改用对象,key 表示打包后输出文件的名字,output.filename 中用占位符表示这个名字:

这种配置一般用于多页应用,且最终会产生多个 chunk。

mode

指定 webpack 进行打包构建的环境是开发环境还是生产环境 —— 根据环境的不同,webpack 会默认开启不同的优化选项。

loader

loader 相当于是一个转换器。webpack 默认只能解析 js / json 文件,对于其他类型的文件需要借助 loader 进行转换,之后才能解析。

plugin

webpack 执行打包构建的生命周期中会触发很多事件,plugin 监听某些事件并执行那些 loader 做不了的特定任务。

易混淆的配置项

filename 和 chunkFilename

filename 很好理解,就是 entry 入口文件对应输出文件的名字,而 chunkFilename 指的是没有被列入 entry 中,但在某些情况下又不得不单独打包输出的文件的名字。

典型的例子就是动态加载模块,比如说入口文件 index.js 如下:

webpack 的配置如下:

打包后,entry 入口文件会对应产生一个叫做 index 的 chunk,同时输出一个叫做 index.js 的 bundle 文件。而 test.js 是动态加载的,它会被单独打包到另一个叫做 test (通过魔法注释指定)的 chunk 中,同时输出一个叫做 test.chunk.js 的文件。

path 和 publicPath

path 很好理解,就是 entry 入口文件对应输出文件的路径,而 publicPath 指的是引用静态资源时的固定前缀。项目上线后通常可以配置 publicPath 为一个 cdn 地址,这样就可以引用部署到 cdn 上的静态资源了。

hash、contenthash 和 chunkhash

配置 bundle 文件名的时候,通常可以使用 xxx.[hash].js 这样的命名形式,这里的占位符可以使用 hash、chunkhash 以及 contenthash。

这三个 hash 通常和 CDN 缓存有关,代码改变触发文件 hash 改变,hash 改变导致资源引用的 URL 改变,从而触发 CDN 回源从服务器重新拉取最新的资源。

以多页面应用为例,假设有 A、B 两个页面。

  • hash:是项目编译层面的 hash,全局一致,任何文件的修改都会导致它发生改变。这意味着 A 页面文件的改变会导致整体 hash 改变,从而影响采用了 hash 命名的 B 页面文件,这样是无法实现静态资源缓存的
  • chunkhash:是 chunk 层面的 hash,每个入口页面对应一个 chunk,其产生的相关 bundle 共用同一个 chunkhash。修改 A 页面文件只会影响 chunk A,不会影响 chunk B
  • contenthash:是单文件层面的 hash,粒度要更精细。假设 A 页面中的 js 引用了 css,那么 js 文件改变所导致的 chunkhash 改变也会作用到 css 文件上,因此这时候的做法是利用 plugin 抽离出 css,并采用 contenthash 命名,标志每一个单独的文件

PS:另外需要注意的是,chunkhash/contenthash 和 HMR 热更新不能一起使用。因为热更新针对的是开发环境,chunkhash 以及 contenthash 针对的是生产环境(涉及到 CDN 缓存)。

资源解析

解析 ES6 语法

安装 babel 相关依赖(preset-env 对应的是 ES6 的 preset):

项目根目录下增加 .babelrc 文件:

增加 module.rules 项,配置 loader:

解析 CSS 和 LESS/SASS/Stylus

以加载和解析 less 文件为例。less-loader 将 less 文件解析为 css 文件,css-loader 解析 css 文件,style-loader 将 css 文件中的样式注入到 style 标签中。

安装:

配置:

解析图片和字体

解析图片或者字体需要用到 file-loader:

url-loader 是对 file-loader 的封装,可以提供 limit 参数:当图片体积小于 limit 参数的时候,使用 url-loader 进行处理,会用 base64 对图片进行编码,通过 dataUrl 引用图片;否则使用 file-loader 进行处理。注意这里一定要设置 esModule: false,否则图片和字体默认会被视为 ES 模块,无法在页面中正常引用。

资源内联

资源内联可以提高项目文件的可维护程度,并减少请求数量。以多页面应用为例,假如每个页面都有公用的 meta 信息,不可能每个 .html 文件都去写一遍,这时候就可以把 meta 信息集中到一个 meta.html 中,在需要用到的页面内联进去即可。

源文件内联:HTML 和 JS

内联 HTML 和 JS 可以使用 raw-loader@0.5.1。

inline.html 文件:

inline.js 文件:

模板 HTML 文件:

构建后生成的 index.html 文件:

PS:这里之所以要使用 0.5.1 版本的 raw-loader,是因为这个版本的 raw-loader 默认采用 CJS 的模块导出方案,所以可以使用 require 直接导入;之后的版本则默认采用 ES Module 导出方案,如果想使用高版本,有两种方法:

  • 通过 <%= require('...').default %> require 一个 ES6 模块
  • 修改 raw-loader 源代码,默认导出方式改为采用 CJS

源文件内联:CSS

两种方案:

  • 方案一:直接使用上面提到的 style-loader,通过 JS 将样式动态注入到 style 中,这种方式下构建产物中不会直接出现样式代码;
  • 方案二:先使用 mini-css-extract-plugin 抽离出 css 文件到构建产物中,并且在 html 文件中通过 link 引用该 css,再使用 html-inline-css-webpack-plugin 将对于 css 文件的 link 引用转化为内联形式。下面介绍第二种方案。

安装:

配置:

注意:

  • 需要同时配置 loader 和 plugin。另外, MiniCssExtractPlugin.loaderstyle-loader 功能上是冲突的,不能一起使用
  • require 的时候需要使用 require(..).default,因为 html-inline-css-webpack-plugin 导出方式是采用 ES Module。
  • 默认情况下,使用了 html-inline-css-webpack-plugin 之后,不会保留由 mini-css-extract-plugin 导出的 css 文件

构建产物内联:CSS 和 JS

前面讲的内联,都是内联 src 下的文件到 html 中,那么有没有办法可以将 bundle 中的 css 和 js 文件内联到 html 中呢?这就要使用 html-webpack-inline-source-plugin 了。

它是 html-webpack-plugin 的一个插件,所以两者都要安装。之后进行配置:

PS:注意必须指定安装 @1.0.0-beta.2 版本的 plugin,npm 默认安装的是旧版本,有 bug。

图片和字体内联

图片和字体的内联,其实就是使用前面提到过的 url-loader,注意需要设置 esModule: false

开发和构建体验

文件监听

文件监听也就是 watch mode。开启文件监听后,每次源代码发生更改,都会自动重新进行构建

文件监听的原理是轮询文件的“最后一次编辑时间”是否改变。ignored 指定忽略监听的文件或者文件夹;poll 表示每秒轮询多少次;此外,并不是文件一更改就马上重新构建,必须是在 aggregateTimeout 指定的时间内没有再次更改之后,才会重新构建,有点类似于做了一层防抖处理,避免频繁构建。

热重载

热重载也就是 live reload,可以在每次源代码发生更改时自动重新进行构建 + 自动刷新浏览器。这里需要使用 webpack-dev-server 实现热重载。

安装:

在 package.json 中配置指令:

配置 webpack-dev-server:

热更新

热重载也有缺点,就是每次都会全局刷新浏览器,所有的状态都会重置。所以在热重载的基础上引入了热更新 —— 也就是 HMR(模块热替换),它既可以实现局部视图刷新,也可以保存数据状态。这里需要使用 webpack 内置的 HotModuleReplacementPlugin 实现热更新。

开启 sourcemap

在生产环境下(mode: production),打包后的文件都是经过压缩的,代码出错后不容易调试。而开启 sourcemap 之后,可以直接定位到出错的源代码,调试就很方便了。

代理跨域请求

本地开发的时候我们会起一个 http://localhost:8080 的服务器,这时候请求后端接口 http://mysite/api/getData会报跨域错误。此时可以通过 webpack-dev-server 配置一层与本地服务器同源的代理服务器,它会接受请求,再将请求转发给真正的后端服务器(同源仅作用于浏览器和服务器之间,所以这个转发是没问题的)。

举个例子,这是我们发起的请求:

这是 webpack-dev-server 的跨域配置:

我们发起请求的 url /api/getData/ 会自动加上同源前缀,变成 http://localhost/api/getData,相当于现在是向同源的代理服务器发起请求;又由于 url 可以匹配 /api,所以这个请求会被进一步转发给真正的后端服务器,相当于发起 http://mysite/api/getData 这个请求。

自动引用构建产物

默认情况下,我们可能需要在 dist 目录下手动创建一个 html 文件去引用构建产物,这是比较麻烦的。通过 html-webpack-plugin,可以在 dist 目录下自动生成一个引用构建产物(资源)的 html 文件。

安装:

配置:

优化构建日志的显示

构建日志中可能包含很多我们并不关心的信息,可以借助 plugin 优化一下。

配置:

捕获构建错误

每次构建结束后会触发 compiler 对象的 done 钩子函数,可以在这个 hook 中捕获构建错误并进行相关处理:

分离配置文件

开发应用的时候一般有两种环境,一个是方便开发调试的开发环境,一个是上线后给用户使用的生产环境。不同的环境,webpack 的配置也不同,比如生产环境需要配置代码压缩,开发环境需要配置热更新等。我们现在是共用一个 webpack.config.js 文件,所以需要解决的问题是:如何根据不同环境使用不同的 webpack 配置文件

方案一: cross-env + NODE_ENV

我们的基本策略是分离三个配置文件:

  • webpack.base.js:开发环境和生产环境共用的配置放在这里
  • webpack.dev.js:开发环境专用的配置放在这里
  • webpack.prod.js:生产环境专用的配置放在这里

node 有一个 process 对象,我们在 process.env 上挂载一个 NODE_ENV 环境变量,用来标记当前是什么环境。因为不同操作系统设置环境变量的方式不同,为了方便统一设置,这里使用 cross-env 这个库。接着,我们在所有文件中都可以通过 node.env.NODE_ENV 获取当前环境类型。

package.json 文件:

webpack.base.js 文件:

webpack.prod.js 文件:

webpack.dev.js 文件:

方案二:配置文件导出函数

基本策略是仍然使用单个配置文件,但是导出的不再是对象,而是函数。运行构建命令的时候传入一个 mode 参数,这个参数被函数接受,从而判断当前环境。

webpack.config.js 文件:

项目开发

处理 CSS

postcss 本身提供了一个强大的插件系统,可以对 css 进行后处理。在 webpack 中,需要通过 postcss-loader 去使用 postcss。

1)自动补齐前缀

为了向下兼容旧浏览器,某些比较新的 css 属性必须加上浏览器厂商的前缀,可以通过 postcss-loader 和 autoprefixer 实现前缀的自动补齐。

安装:

配置:

2)单位转换

移动端的适配分为两步:

  • 根据屏幕分辨率动态设置根元素的字体大小。这里可以使用手淘的 lib-flexible 或者 viewport 单位来实现,这样,1rem 的大小就是动态的了
  • 根据设计稿的 px 进行开发,最后通过插件将 px 统一转化为当前开发使用的分辨率下对应的 rem。

这里进行单位转化的插件,就可以使用 px2rem-laoder、postcss-plugin-rem 等来完成。

代码规范

集成 eslint

1)单独使用

单独使用 eslint,首先需要安装 eslint 本体:

生成 eslint 配置文件 .eslintrc.json:

根据我们回答的问题,会预先配置好文件中的一些选项。如果想要使用其它公司团队的 eslint 规范,需要单独安装:

并修改配置文件中的 extends 选项:

运行下面命令对指定文件进行 lint 检测:

2)集成到 webpack 中使用

在 webpack 中集成 eslint 有两种方式,一种是 eslint-loader,但它存在一些问题,不久将被弃用;webpack 5 开始更推崇使用 eslint-webpack-plugin。这里以后者为例。

首先安装:

接着到项目根目录下新建 .eslintrc.json 文件,内容和上面差不多。如果想要使用某个公司团队的 eslint 规范,同样需要单独安装 npm 包。

然后配置 webpack.config.js:

基本使用就是这样了,每次运行构建 eslint 都会检测代码。默认情况下 eslint 的报错信息采用的是 stylish 的展示风格,可能不太直观,可以使用特定的插件修改报错信息的展示风格。安装:

配置:

集成 stylelint

在 webpack 中集成 stylelint 有三种方式:

  • 使用 stylint-loader(官方已弃用,不推荐)
  • 使用 postcss-loader + stylelint
  • 使用 stylelint-webpack-plugin

这里以第三种方式为例。首先安装:

在项目根目录下新建 .stylelintrc.json 文件:

配置:

每次运行构建 stylelint 都会检测代码。

PS:以上两种 lintPlugin 的安装都不需要额外安装 eslint 或者 stylelint,因为 npm 从 v7 开始会自动安装 peerDependencies。

搭建 Vue 开发环境

vue-cli 集成了 vue 和 webpack,但还是有必要掌握如何用 webpack 搭建一个基础的 Vue 开发环境。

创建目录并安装相关依赖:

新建 src 文件夹,src/App.vue 是 SFC 单文件组件:

src/index.js 作为打包入口:

src/index.html 作为构建产物使用的模板 html:

配置 webpack:

注意关于 vue 的几个依赖的作用:

  • vue-loader:用于提取 .vue 中的各个语言块
  • VueLoaderPlugin:将 webpack 声明的规则应用于对应的语言块,比如 css-loader、style-loader 会同时作用在 .vue 文件中的 <style></style> 语言块
  • vue-template-compiler:将 template 预编译成函数,避免运行时进行模板编译,从而加快应用运行速度

项目打包

多页面应用打包

对于多页面应用来说,每个页面都对应“一个 entry + 一个 HtmlWebpackPlugin 实例”。如果每次添加或者删除页面都需要重新配置那就太麻烦了,因此理想的方案是根据页面情况实现自动配置。

假设项目结构如下:

page1 和 page2 目录下都有一个 index.js 表示页面入口,index.html 表示页面的模板 html。

首先安装 glob 方便读取文件路径:

通过一个 setMPA 函数处理多页面应用配置:

调用 setMPA 生成配置对象,注入到 webpack 的配置中:

基础组件库打包

以打包 + 发布一个 promise 为例。

新建 ./src/index.js ,编写核心代码。接着新建 webpack.config.js 进行打包配置:

新建 ./index.js 作为该库的入口文件:

打包:

发布:

使用:

首先正常安装

在需要的文件中导入使用:

webpack 性能优化

如何进行性能分析

webpack 本身可以配置 stats 展示打包构建的信息,但这些信息颗粒度比较大,不利于进行性能分析。所以这里还是要借助插件来分析。

分析构建速度

性能分析其一,用 speed-measure-webpack-plugin 分析构建速度,它可以分析每个 loader 和 plugin 的耗时。安装后配置如下:

耗时长的 loader 或者 plugin 会显示为红色,这也是我们需要关注并优化的重心。

分析打包体积

性能分析其二,用 webpack-bundle-analyzer 分析打包体积,在浏览器的 8888 端口下可以看到每个文件的体积信息以及各个 chunk 的包含关系,方便我们进行分析。

PS:安装这个 npm 包可能会报很恶心的 node gyp 错误,可以尝试切换回 npm 原来的镜像源(不要使用淘宝镜像源)

文件结构优化

文件结构优化指的是要合理地拆分代码文件。为什么要拆分代码文件呢?一般是从两个角度考量:

  • 更好地利用缓存:假如 css 没有从 js 文件中分离出来,那么每次 js 或者 css 改变,用户都得重新下载整个文件;而分离之后,两者独立,一方改变后,另一方的缓存仍可利用,无需重新下载
  • 更好地复用代码:如果开发的是多页面应用,可以把公共样式单独提取成一个文件,这样公共样式文件只需要下载一次,而不是每进入一个页面就要重复下载
合理使用动态加载

通过 import() 或者 require.ensure() 对某些体积较大的模块实现按需加载、动态加载的时候,这些模块会打包到单独的文件中。如果用户用不到这个模块,那么他们就无需加载它,不再像之前那样一股脑地加载整个代码文件。

多页面应用使用动态路由

对于多页面应用,采用之前提到的多页面应用打包方案,使每个页面都有自己对应的文件,这样用户在进入某个页面的时候,只需要加载和这个页面相关的资源,而不是全部一次性加载。

splitChunks 代码分割

形成 chunk 的方法有三种:

  • 设置多个 entry 入口点,每个 entry 会被打包到一个 chunk 中
  • 动态导入某些代码,这些代码会被打包到一个 chunk 中
  • 通过 splitChunks 分割代码,分割的代码会被打包到一个 chunk 中

通过配置 optimization.splitChunks.cacheGroups ,可以将公用的第三方库代码抽离成一个单独的 chunk,下面通过一个例子来理解这个过程。

假设我们的应用有两个页面,对应两个 entry 入口文件:

webpack 配置如下:

chunks: "async"

chunks 的默认值就是 async,表示会将异步导入(动态导入)的模块抽离成单独的 chunk。

对于 page1.js:本身 entry 文件就会对应一个 chunk,而 jq 和 react 都是同步导入的,因此不会从这个 chunk 中分离,它们三个最终会打包到一起,并输出到 page1.bundle.js 文件。而 lodash 是动态导入的,会分离到一个单独的 chunk 中,并输出到 vendors~page1-lodash.js 文件

对于 page2.js:本身 entry 文件就会对应一个 chunk,而 jq 是同步导入的,因此不会从这个 chunk 中分离,它们两个最终会打包到一起,并输出到 page2.bundle.js 文件。而 lodash 是动态导入的,它会和 page1.js 中同样动态导入的 lodash 一起打包到同一个 chunk 中,最终输出到 vendors~page1-lodash.js 文件。react 也是动态导入的,它也会打包到一个单独的 chunk 中,最终输出到 vendors~page2-react.js 文件

综上,最终会有 4 个 chunk,输出到 4 个 bundle 文件中。从控制台打印信息可以看出确实是这样的:

借助 BundleAnalyzePlugun 可以更加直观地分析 bundle 的成分,如下图:

chunks: "initial"

initial 表示,不管模块是同步导入还是异步导入,都会被抽离成单独的 chunk。如果不同的 chunk 都通过同步导入的方式共用了同一个模块,则这两个模块可以被抽离到同一个 chunk 中。

  • 首先还是同样的,page1.js 和 page2.js 本身的 entry 文件会分别对应两个 chunk,并输出到 page1.bundle.js 和 page2.bundle.js 中。
  • 由于两个 chunk 都同步导入了 jq,因此 jq 最终被抽离到一个 chunk 中,并输出到 vendors~page1-page2.js 文件。
  • 对于都异步导入的 lodash 也是一样,会输出到 page1-lodash.js 文件。
  • 而对于 react 的处理就不同了,虽然两个文件都导入了 react,但一个是同步导入,一个是异步导入,这种情况下,react 会被分别抽离到两个 chunk 中,同步导入的 react 输出到 vendors~page1.js,异步导入的 react 输出到 page2-react.js。

综上,最终会有 6 个 chunk,输出到 6 个 bundle 文件中。

控制台打印信息如下:

BundleAnalyzePlugun 的分析情况如下:

PS:使用 chunks:"initial" 的时候需要注意,会有一个 minSize 字段表示被抽离成单独 chunk 的模块至少需要多大,如果模块体积本身小于这个值,则它也不会被单独抽离成 chunk,而是和 entry 对应的 chunk 打包在一起。

chunks: "all"

all 的特点在于,只要两个 chunk 共用了同一个模块,则不管模块在各自的 chunk 中是同步导入还是异步导入,最终都可以被抽离到同一个单独的 chunk 中。

  • page1.js 和 page2.js 本身的 entry 文件会分别对应两个 chunk。
  • 由于这两个 chunk 共用了 jq,所以 jq 被抽离到一个单独的 chunk 中,最终输出到 vendors~page1-page2.js
  • 由于这两个 chunk 共用了 lodash,所以一样的,被抽离到一个 chunk 中,最终输出到 vendors~page1-lodash.js
  • 对于 react,虽然在各自 chunk 中导入方式不同,但确实是属于共用的模块,所以也会被抽离到一个 chunk 中,最终输出到 vendors~page1-page2-react.js

控制台打印信息如下:

BundleAnalyzePlugun 的分析情况如下:

从这三种设置的结果可以看出,chunks:"all" 是可以最大程度复用代码的,因为在它的规则下,只要是模块被共用了,就可以被抽离到同一个 chunk 中。

构建速度优化

减小文件搜索范围

webpack 打包构建的过程中会进行很多搜索过程,如果可以通过修改配置减小文件搜索的范围,那么就可以提高构建速度。

从配置 noParse 的角度来说:

默认情况下,我们导入 jq 或者 lodash 这样的库时,webpack 会去递归地解析这些库是否有其他第三方依赖。这个过程其实是不必要的,所以可以通过 noParse 配置不需要递归解析的模块:

从配置 resolve 的角度来说:

resolve.alias

可以配置路径的别名,减少类似 import xxx from '../../a/b' 这样繁琐的导入语句。不仅开发上更加方便,而且 webpack 解析到别名的时候,可以直接去对应的目录找到模块。

使用:

注意,在 HTML 或者 CSS 中使用别名路径的时候,必须加 ~ 前缀。另外,必须安装 html-loader 和 css-loader,webpack 才能正确解析别名路径对于资源的引用。

resolve.extensions

提供一个后缀名数组,如果像 import 这样的导入语句省略了文件后缀名,则会为文件依次加上数组中的后缀名,看文件是否存在。这意味着我们经过配置后,在导入语句中可以省略文件的后缀名。

一般来说,应该将出现频率较高的后缀名写在前面,加快 webpack 解析时的匹配速度。另外不应该配置并省略过多的后缀名,否则会增加 webpack 解析时的查找时间。

resolve.modules

指定 webpack 去哪些路径下查找模块,默认会从项目根目录开始找,找不到就往外层找。一般我们自己写的模块或者第三方模块都在项目根目录下了,所以可以指定一下目录,减少不必要的向外查找。

resolve.mainFields

指定的是去查找第三方模块的 package.json 文件的哪些字段,从而找到模块的入口文件。一般来说入口文件都配置在 main 字段中,所以可以直接将其配置为 ['main']

从配置 loader 的角度来说

可以排除掉一些不需要解析的文件,或者精准指定需要解析的文件,从而减小解析时间,加快构建速度。

以 babel-loader 为例,默认情况下它会解析根目录中的所有 js 文件,但实际上,node_modules 中的很多第三方包本身就已经经过处理了,无需再进行解析,那么这部分就可以排除掉;同时,我们需要解析的通常是自己编写的代码,所以可以明确指定解析 src 目录下的文件:

多进程并行构建

webpack@4 之前使用 HappyPack,webpack@4 之后使用 thread-loader。安装后配置:

排在 thread-loader 之后的那些 loader 会被放在一个进程池中单独运行。这里需要注意,进程池中的 loader 不能产生新文件,因此类似 MiniCssExtractPlugin.loader 这样产生 css 文件的 loader 是不能使用 thread-loader 的。

PS:不管是 HappyPack 还是 thread-loader 都只适用于大型项目,小型项目中的优化很不明显,甚至可能反而降低打包构建的速度。

多进程并行压缩

CSS 和 JS 的压缩可以开启多进程并行压缩(默认开启):

利用缓存提升二次构建速度

前面讲到的都是提升首次构建速度,我们可以将首次构建的结果缓存下来,然后利用缓存提升二次构建的速度。

开启 babel-loader 的缓存功能:

开启 terser-webpack-plugin 的缓存功能:

或者使用 cache-loader 缓存构建结果:

或者使用 hard-source-webpack-plugin 缓存构建结果:

打包体积优化

优化打包体积即减小打包体积,基本策略是对文件体积进行压缩,或者设法减少文件的数量。

webpack 自带:Tree-Shaking

tree-shaking 就是所谓的摇树优化,可以实现 DCE,即去除没有用到的代码,包括:

  • 永远不会执行的、不可达的代码
  • 执行结果没有被用到的代码
  • 只会影响死变量(指变量赋值了,但之后再也没有读取)的代码

在讲 tree-shaking 之前,首先要理解静态分析的概念。静态分析指的是不需要实际执行代码,仅从字面量就可以对代码进行分析,诸如 import() 和 CommonJS 的 require() 都是动态的,而 ESModule 则不一样,它支持静态分析 —— 在使用 ESModule 的时候,模块是否导入、是否导出、模块之间的依赖关系,这些都是可以提前确定好的,而这种静态分析的特性正是实现 tree-shaking 的关键。

tree-shaking 如何发挥作用呢?以下面的代码为例:

虽然只是导入并使用了 a,但实际上最终 a 和 b 都会被打包到 bundle 中,这会无形增加代码体积。但是如果使用了 tree-shaking,则最终只有 a 会被打包。

同样的,如果是下面这种情况:

使用了 tree-shaking 之后,由于没有用到 b,所以最终 b 也不会被打包。

对于有副作用的代码(会向外界产生可观察的变化),tree-shaking 无法将其修剪掉。如果确定自己的项目没有副作用,可以配置 webpack.config.js 的 optimization.sideEffects: true(生产环境自动开启),同时配置 package.json 的 sideEffects: false ,告知 webpack 无需考虑副作用的问题,可以放心进行 tree-shaking;另外也可以指定一个路径数组,明确告知 webpack 哪些文件有副作用,从而让 webpack 为其它没有副作用的文件进行 tree-shaking 处理。

前面也说过,tree-shaking 依赖于 ESModule 的静态分析,那么对于 lodash 这样不使用 ESModule 模块规范的第三方库,怎么进行 tree-shaking 呢?这时候可以考虑使用这种库的 es 版本,比如 lodash 对应的就有一个 lodash-es 版本。

webpack 在生产环境下默认开启 tree-shaking,当然也可以手动开启:

webpack 自带:Scope-Hoisting

scope-hoisting 可以将多个函数声明压缩为一个,这样做的好处一个是减少声明语句,从而减小代码体积;一个是减少函数作用域数量,从而降低内存开销。

webpack 在生产环境下默认开启 scope-hoisting,当然也可以手动开启:

资源优化:压缩 HTML

由 html-webpack-plugin 生成的 html 文件,可以通过设置 minify: true 开启压缩功能(生产环境下默认开启)。

资源优化:压缩 JS

webpack 默认内置了 uglifyjs-webpack-plugin或者 terser-webpack-plugin,并且在生产环境下自动开启,因此 JS 代码默认就是经过压缩的。

当然也可以自定义配置(具体配置项需要参考 terser):


上面指的是对 JS 代码进行压缩,还有一种减少 JS 代码的策略是动态引入 polyfill。

babel 所做的事情只是转换语法,比如 const 转化为 var,箭头函数转化为普通函数等,对于诸如 map、Promise 这样比较新的 api 则无法进行处理,这时候就需要借助 polyfill 实现向下兼容。但是单纯使用 babel-polyfill 的问题在于,任何时候都是全量引入的,而有些用户的浏览器比较新,其实用不着使用 polyfill

所以如果能实现动态引入 polyfill,也可以减少代码体积。这里借助的是 polyfill-service,我们引用它提供的 polyfill CDN,对于不同的浏览器,它会返回不同版本的 polyfill。

资源优化:压缩 CSS

有三种方案,不管是哪一种,底层使用的压缩引擎都是 cssnano,而 css-loader 已经内置了 cssnano,因此无需额外安装。

方案一:postcss+ cssnano

需要安装 postcss-loader,接着进行配置:

方案二:optimize-css-assets-webpack-plugin + cssnano

适用于 webpack@5 之前的版本。需要安装 optimize-css-assets-webpack-plugin,接着进行配置:

方案三: css-minimizer-webpack-plugin

适用于 webpack@5 之后的版本。需要安装 css-minimizer-webpack-plugin,接着进行配置:


上面的 css 压缩都基于 cssnano,它很好用,但无法移除那些没有使用过的样式。这里可以使用 purgecss-webpack-plugin 实现 css 中的 tree-shaking。

注意这里的 path 并不是指要移除无用样式的 css 所在的路径,而是指引用这个 css 的文件所在的路径,可以直接配置为 src 下的所有文件。purgecss 会对这些文件进行分析,最终产出一个移除了无用样式的 css 文件。

资源优化:处理图片

从减小文件数量的角度来说:

1)可以使用前面提到的 url-loader,对体积小于 limit 的图片进行 base64 编码,转化为 dataUrl 内联进我们的应用

2)对于 svg 图片,可以使用类似的 svg-url-loader 对其进行 utf-8 编码,转化为 dataUrl 内联进应用。它相比 base64 编码的优势在于,字符串更短,浏览器解析更快

3)对于图标类图片,可以使用 postcss-loader + postcss-sprites(需要额外安装 phantomjs),它可以将多张图片合并成一张雪碧图,并且自动调整图片的背景位置。

大致配置如下:

postcss-sprites 作用的原理可以简单理解为,将 css 文件中引用到的图片资源合并成一张雪碧图,并自动处理背景图的展示位置。经由 file-loader 处理后,最后产出的 bundle 中只包含雪碧图这一张图片。

这里需要注意,spritePath 配置的是雪碧图的存放路径。一般雪碧图放在 src 中而不是 dist 中,因为 dist 中本来就会在 file-loader 的作用下产出图片,没有必要重复导出雪碧图到 dist 中 —— 即使导出了,也属于没有被使用的静态资源,会被 clean-webpack-plugin 清理掉。

此外,postcss-sprites 这个插件不支持识别 resolve.alias 配置的别名。


从减小文件大小的角度来说,对大体积的图片可以使用 image-webpack-loader 进行无损压缩。这个 loader 必须在 file-loader 之前处理图片,所以最好配置 enforce: 'pre'

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-09-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 安装
  • 核心概念
    • module、chunk 和 bundle
      • entry 和 output
        • mode
          • loader
            • plugin
            • 易混淆的配置项
              • filename 和 chunkFilename
                • path 和 publicPath
                  • hash、contenthash 和 chunkhash
                  • 资源解析
                    • 解析 ES6 语法
                      • 解析 CSS 和 LESS/SASS/Stylus
                        • 解析图片和字体
                        • 资源内联
                          • 源文件内联:HTML 和 JS
                            • 源文件内联:CSS
                              • 构建产物内联:CSS 和 JS
                                • 图片和字体内联
                                • 开发和构建体验
                                  • 文件监听
                                    • 热重载
                                      • 热更新
                                        • 开启 sourcemap
                                          • 代理跨域请求
                                            • 自动引用构建产物
                                              • 优化构建日志的显示
                                                • 捕获构建错误
                                                  • 分离配置文件
                                                    • 方案一: cross-env + NODE_ENV
                                                    • 方案二:配置文件导出函数
                                                • 项目开发
                                                  • 处理 CSS
                                                    • 代码规范
                                                      • 集成 eslint
                                                      • 集成 stylelint
                                                    • 搭建 Vue 开发环境
                                                    • 项目打包
                                                      • 多页面应用打包
                                                        • 基础组件库打包
                                                        • webpack 性能优化
                                                          • 如何进行性能分析
                                                            • 分析构建速度
                                                            • 分析打包体积
                                                          • 文件结构优化
                                                            • 合理使用动态加载
                                                            • 多页面应用使用动态路由
                                                            • splitChunks 代码分割
                                                          • 构建速度优化
                                                            • 减小文件搜索范围
                                                            • 多进程并行构建
                                                            • 多进程并行压缩
                                                            • 利用缓存提升二次构建速度
                                                          • 打包体积优化
                                                            • webpack 自带:Tree-Shaking
                                                            • webpack 自带:Scope-Hoisting
                                                            • 资源优化:压缩 HTML
                                                            • 资源优化:压缩 JS
                                                            • 资源优化:压缩 CSS
                                                            • 资源优化:处理图片
                                                        相关产品与服务
                                                        内容分发网络 CDN
                                                        内容分发网络(Content Delivery Network,CDN)通过将站点内容发布至遍布全球的海量加速节点,使其用户可就近获取所需内容,避免因网络拥堵、跨运营商、跨地域、跨境等因素带来的网络不稳定、访问延迟高等问题,有效提升下载速度、降低响应时间,提供流畅的用户体验。
                                                        领券
                                                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档