前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

dll 预编译提高 webpack 打包速度,以及它现在的替代方案

首页2018-11-23 11:10:21Front-End
Webpackdll构建优化打包速度

一个上了 antd、react 全家桶、moment、lodash 的中台项目,改一行业务代码,npm run build 要跑一分多钟。看构建日志会发现,绝大部分时间花在了那些一个月都不会动一次的第三方库上,每次都要重新解析、转译、压缩一遍。

DllPlugin 要解决的就是这件事:把第三方库提前单独打一次包,之后的每次构建直接引用现成的产物,跳过整个编译过程。这篇把原理、三步接入、踩过的坑讲一遍,最后说清楚 webpack 5 上为什么不再推荐这套做法,以及该换成什么。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 为什么 CommonsChunkPlugin 分了 vendor,改业务代码 vendor 的 hash 还是会变
  • dll 的思路是从哪来的,manifest.json 里存的到底是什么
  • 三步接入 DllPlugin 和 DllReferencePlugin,以及每一步的坑
  • happypack 多线程打包怎么配,什么时候是负优化
  • webpack 5 用 cache: { type: 'filesystem' } 替代 dll 的完整写法和迁移思路

# 一、webpack 的 dll 功能

下面这套配置基于 webpack3 构建,webpack 4 上写法基本一致,webpack 5 的情况放在最后单独讲。

# 1.1 dll 介绍

我们构建前端项目的时候,往往希望第三方库(vendors)和自己写的代码可以分开打包,因为第三方库往往不需要经常打包更新。对此 Webpack 的文档建议用 CommonsChunkPlugin 来单独打包第三方库。

这里要先分清两件事,很多人一开始会混。

我们这里的 dll.js 是提前打包好了的,而不是在每次 build 的时候去打包输出的,这样才能做到依赖包一次构建、无限次使用。另外,webpack 输出的文件名都带有 hash 值,而用 dll 构建后输出的文件名是固定的。

先看 CommonsChunkPlugin 那种做法长什么样:

entry: {
  vendor: ["jquery", "other-lib"],
  app: "./entry"
}
new CommonsChunkPlugin({
  name: "vendor",

  // filename: "vendor.js"
  // (Give the chunk a different name)

  minChunks: Infinity,
  // (with more entries, this ensures that no other module
  //  goes into the vendor chunk)
})
@前端进阶之旅: 代码已经复制到剪贴板

这份配置确实把第三方库单独拆成了一个 vendor chunk,看起来目的达到了。但它有个致命问题。

通常为了对抗缓存,我们会给输出文件的文件名中加入 hash 后缀。问题就出在这里,我们编辑了 app 部分的代码后重新打包,会发现 vendor 的 hash 也跟着变了。

改动 app 代码后 vendor 的 hash 也跟着变化

这么一来,每次发布版本的时候 vendor 代码都要刷新,即使我并没有修改其中的代码。这样并不符合我们分开打包的初衷。

那为什么改业务代码会让 vendor 改名呢?两个原因叠在一起。一是 [hash] 是整次构建的哈希,只要有任何文件变了,所有产物一起改名,得换成 [chunkhash] 才是按 chunk 内容算;二是 webpack 的运行时代码里存着模块 id 到 chunk 的映射表,业务代码一变这张表就变,而这段 runtime 默认是塞在 vendor 里的。所以就算换了 [chunkhash],不把 runtime 单独提出去,vendor 照样会变。

dll 走的是另一条路,它干脆让 vendor 不参与每次构建。

Dll 这个概念应该是借鉴了 Windows 系统的 dll。一个 dll 包,就是一个纯纯的依赖库,它本身不能运行,是用来给你的 app 引用的。打包 dll 的时候,Webpack 会将所有包含的库做一个索引,写在一个 manifest 文件中,而引用 dll 的代码(dll user)在打包的时候,只需要读取这个 manifest 文件就可以了。

优势

  • Dll 打包以后是独立存在的,只要其包含的库没有增减、升级,hash 也不会变化,因此线上的 dll 代码不需要随着版本发布频繁更新
  • App 部分代码修改后,只需要编译 app 部分的代码,dll 部分,只要包含的库没有增减、升级,就不需要重新打包。这样也大大提高了每次编译的速度
  • 假设你有多个项目,使用了相同的一些依赖库,它们就可以共用一个 dll

第三条在多项目的团队里特别香。几个中台系统用同一套技术栈,dll 打一份放 CDN,所有项目共用,用户从第二个系统开始就是直接命中缓存。

# 1.2 dll 使用

整个流程分三步,先建一个 dll 的配置文件(entry 只包含第三方库),再加一条构建命令,最后在业务配置里把 dll 关联进来。

第一步:新建 webpack.dll.conf.js

webpack.DllPlugin 的选项中,path 是 manifest 文件的输出路径,name 是 dll 暴露的对象名,要跟 output.library 保持一致。这两个对不上的话,运行时会直接抛 undefined,而且报错信息完全看不出原因,这个坑我踩过。

// build/webpack.dll.conf.js

const path = require('path')
const webpack = require('webpack')

module.exports = {
  entry: {
    // 把这些资源打包成dll,提高编译速度
    react: ['react','react-router-dom','redux','redux-immutable','immutable','react-redux','react-router','redux-logger','redux-thunk','styled-components'],
    ui: ['antd-mobile','antd'],
    others: ['react-icons','axios','clipboard','humps','lodash','md5','moment','normalizr']
  },
  output: {
    path: path.resolve(__dirname, "../dist/static/js"),
    filename: '[name].dll.js',
    library: '[name]_library'
  },
  plugins: [
    new webpack.DllPlugin({
      path: path.join(__dirname, '../dist/static/js/[name].manifest.json'),
      name: '[name]_library'
    }),
    new webpack.optimize.UglifyJsPlugin()
  ]
}
@前端进阶之旅: 代码已经复制到剪贴板

这里说明一下,我原来的笔记在 plugins 里写的是 DllReferencePlugin 加一段 Object.keys(['react','ui','others']).map(...),那是错的。DllReferencePlugin 是给业务配置用的,dll 配置里该用 DllPlugin;而 Object.keys 作用在数组上返回的是 ['0','1','2'] 这样的下标,不是包名。上面已经改成正确写法了。

entry 按「更新频率」分组是个好习惯。React 全家桶、UI 库、工具库各一组,某个库升级时只需要重打对应那一组,不用全量重来。

第二步:加一个命令

// package.json
"scripts": {
  "dll": "webpack --config config/webpack.dll.conf.js"
}
@前端进阶之旅: 代码已经复制到剪贴板

执行 npm run dll。

运行 Webpack 之后会输出两类文件,一类是打包好的 [name].dll.js,一类是对应的 manifest.json,长这样:

{
  "name": "vendor_ac51ba426d4f259b8b18",
  "content": {
    "./node_modules/antd/dist/antd.js": 1,
    "./node_modules/react/react.js": 2,
    "./node_modules/react/lib/React.js": 3,
    "./node_modules/react/node_modules/object-assign/index.js": 4,
    "./node_modules/react/lib/ReactChildren.js": 5,
    "./node_modules/react/lib/PooledClass.js": 6,
    "./node_modules/react/lib/reactProdInvariant.js": 7,
    "./node_modules/fbjs/lib/invariant.js": 8,
    "./node_modules/react/lib/ReactElement.js": 9,
    
    ............
@前端进阶之旅: 代码已经复制到剪贴板

Webpack 将每个库都进行了编号索引,之后的 dll user 可以读取这个文件,直接用 id 来引用。

manifest.json 里记录的模块路径与 id 索引

理解这张表是理解 dll 的关键。业务代码构建时遇到 import React from 'react',webpack 会先去 manifest 里查这个路径在不在,在的话就不再解析这个模块,直接生成一句「去全局变量 react_library 里取 id 为 2 的那个模块」。整个解析、转译、压缩的过程都跳过了,省下来的就是这部分时间。

第三步:在 plugins 中增加配置

// build/webpack.prod.conf.js
module.exports = {
   plugins: [
        new webpack.DllReferencePlugin({
          manifest: require('../dll/react-manifest.json')
        }),
        new webpack.DllReferencePlugin({
          manifest: require('../dll/ui-manifest.json')
        }),
        new webpack.DllReferencePlugin({
          manifest: require('../dll/others-manifest.json')
        })
   ]
}
@前端进阶之旅: 代码已经复制到剪贴板

有几个 dll 分组就 new 几个 DllReferencePlugin,一个都不能漏,漏了那一组就会被重新编译进业务包。

还有一步文章里容易漏掉:dll 产出的 [name].dll.js 不会自动出现在页面上,得自己在 HTML 里用 <script> 引入,而且要排在业务脚本前面。用了 html-webpack-plugin 的话,可以配合 add-asset-html-webpack-plugin 自动注入。忘了这一步的表现是构建成功但页面一片空白,控制台报某个全局变量 undefined。

再次执行 npm run build。

之前

接入 dll 之前的构建耗时

之后

接入 dll 之后的构建耗时明显下降

时间降下来了,降幅取决于你的第三方依赖有多重,依赖越重收益越大。

fe
  • 一、webpack 的 dll 功能
    • 1.1 dll 介绍
    • 1.2 dll 使用
  • 二、happypack 多线程打包
  • 三、webpack 5 之后,dll 该换成什么
  • 总结
  • 参考

← webpack4升级篇ESLint 配置文件详解 从 eslintrc 字段到扁平配置 →