组件库的 webpack 构建速度优化
背景
在公司的主要工作是组件库 (基于 vue 的 ui 组件库, 类似 element-ui) 的开发, 也已经有两个多月, 期间一直觉得项目的开发构建太慢, 每次开发打开开发环境需要 40s 左右, 简直不能忍前前后后尝试了各种优化手段, 但是都不理想终于在今天, 找到了问题所在, 构建速度提升了 50% 以上, 现在只需要 17s 左右, 整个心情都好了现在记录一下所用到的各种优化手段, 因为是开发环境, 所以只考虑构建速度
各种配置项的优化
主要是对一些 loader 添加 include exclude 之类的小优化, 其实这一点并没有带来多少性能的提升, 只是一些安慰作用吧
引入 happypack
修改 webpack 一些 loader 配置, 使用 happypack
- // config.dev.js
- {
- // ...
- module: {
- rules: [{
- test: /\.vue$/,
- loader: 'vue-loader',
- options: {
- css: 'style-loader!css-loader!sass-loader',
- // vue 文件中基本不存在 css 代码, 所以只把 js 交给 happypack 处理
- js: 'happypack/loader?id=babel'
- }
- }, {
- test: /\.js$/,
- use: 'happypack/loader?id=babel',
- exclude: /node_modules/,
- // components 目录是组件, examples 目录主要是 markdown 文档, test 目录是单元测试
- include: [utils.resolve('./components'), utils.resolve('./examples'), utils.resolve('./test')]
- }, {
- test: /\.scss$/,
- use: 'happypack/loader?id=scss'
- }]
- },
- plugins: [
- new HappyPack({
- id: 'babel',
- threads: 4,
- loaders: ['babel-loader']
- }),
- new HappyPack({
- id: 'scss',
- threads: 4,
- loaders: [
- 'style-loader',
- 'css-loader',
- {
- loader: 'postcss-loader',
- options: {
- config: {
- path: utils.resolve('./postcss.config.js')
- }
- }
- },
- 'sass-loader'
- ]
- })
- ]
- // ...
- }
这里主要将组件库中各种需要处理的文件都采用 happypack 处理, 除了上面的 js scss vue 之外, 也把 md (vue-markdown-loader)等进行处理, 配置都差不多, 就不列出来了
ok, 配置完毕, 赶紧跑起来试试水结果就是报错了... oh no! 看了下官方文档说明, 不支持
vue-markdown-loader
好吧, 把 md 文件的处理改回去, 再跑嗯这次跑起来了, 但是时间也就少了 4s-5s 左右, emmmmm, 并没想象中多
将运行的命令加上 --progress 能发现, 主要耗时的就是处理 md 文件, 很明显一遇到 md 文件进度条的跳动就慢下来了
知道了, 优化的主要目标应该是 md 文件的处理
找到了 build/util.js 里面的一些处理, 部分代码如下
- function render(tokens, idx) {
- // tokens 是 markdown-it parse 后的结果
- var m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
- if (tokens[idx].nesting === 1) {
- let index = idx + 1;
- var html = '';
- var style = '';
- var script = '';
- while (tokens[index].nesting === 0) {
- const content = tokens[index].content;
- const tag = tokens[index].info;
- if (tag === 'html') {
- html = convert(striptags.strip(content, ['script', 'style'])).replace(
- /(<[^>]*)=""(?=.*>)/g,'$1'
- );
- script = striptags.fetch(content, 'script');
- style = striptags.fetch(content, 'style');
- } else if (tag === 'js' && !script) {
- script = striptags.fetch(content, 'script');
- } else if (
- ['css', 'style', 'scss'].indexOf(tag) !== -1 &&
- !style
- ) {
- style = striptags.fetch(content, 'style');
- }
- index++;
- }
- var description = m && m.length > 1 ? m[1] : '';
- var jsfiddle = { html: html, script: script, style: style };
- var descriptionHTML = description ? md.render(description) : '';
- jsfiddle = md.utils.escapeHtml(JSON.stringify(jsfiddle));
- return `
- <demo-block class="demo-box" :jsfiddle="${jsfiddle}">
- <div class="source" slot="source">${html}</div>
- ${descriptionHTML}
- <div class="hljs highlight" slot="highlight">
- `;
- }
- return '</div></demo-block>\n';
- }
主要是将 tip 放到指定的 container 里还有提取 tokens 里一些标记为 html js css 代码组成一个对象 jsfiddle, 传给一个 vue 组件, 用于提供 jsbin 的在线调试功能利用 markdown-it 的 render 方法, 将其他采用 markdown 语法写的文档 render 成 html 代码放到指定 div 里面, 将 html 代码 (其实就是文档中的示例代码) 作为 slot 分发给上面提到的 vue 组件
这里实在是没找到优化的手段
引入 dll
另外一个尝试的手段是, 采用 webpack 的 DllPlugin 和 DllReferencePlugin 引入 dll, 让一些基本不会改动的代码先打包成静态资源, 让 webpack 少处理一些东西
打包 dll 的配置
- // config.dll.js
- module.exports = merge(base, {
- // ...
- entry: {
- vendor: ['vue', 'vue-router', 'vue-i18n', 'clipboard']
- },
- output: {
- path: path.resolve(__dirname, './dll'),
- filename: '[name].js',
- library: '[name]_[hash]'
- },
- plugins: [
- new webpack.DllPlugin({
- name: '[name]_[hash]',
- path: path.resolve(__dirname, './dll/vendor.manifest.json')
- })
- ]
- // ...
- })
上面配置打包会在 build 目录下生成 dll 目录, 里面有 vendor.dll.js 和
vendor.manifest.json
然后在 config.dev.js 中, 引入 DllReferencePlugin, 就可以了
DllReferencePlugin 配置
- {
- plugins: [
- new webpack.DllReferencePlugin({
- manifest: require('./dll/vendor.manifest.json')
- })
- ]
- }
这样, 在项目中 webpack 处理 vue vue-router vue-i18n clipboard 时, 就不会去 node_modules 中拿了, 会直接用 vendor.js
再次运行 npm run dev 发现时间也只少了 1s(我觉得其实是时间的小波动... 根本不会少的) 毕竟大头不在这
单组件的开发模式
后来突然想到, 好像每次开发组件的时候, 不都是单个单个来的吗, 既然这样, 我只处理指定组件的 md 文件, 速度不就起来了吗
嗯, 这或许是个办法, 试试水
找到引入 md 文件的地方, 也就是 examples/route.js, 部分代码如下
- function loadDocs(path) {
- return r => require.ensure([],
- () => r(require(`./docs${path}.md`))
- );
- }
这个是 vue-router 的动态加载, 嗯, 只要我把 path 给写成一个固定的路径(这里其实就是'/' + 组件名), 不就能实现了吗
运行命令大概是长这样的
- // package.json
- {
- "scripts": {
- "dev:component": "cross-env RUN_ENV=component node build/dev-server.js",
- }
- }
然后采用 webpack 的 DefinePlugin 动态写入一个组件名就搞定了, 大致的思路是这样的部分实现如下:
- const component = process.argv[2];
- // 先判断一下是不是单组件开发模式, 是的话, 必须指定运行的组件
- if (process.env.RUN_ENV === 'component' && !component) {
- throw new Error('component is required, like: npm run dev:component slider');
- }
- // 然后通过 DefinePlugin 写入
- // 对了这里有个要注意的点, path 是个变量, 不是字符串, 所以不能是 "'path'", 真 tm 机智
- // config.dev.js
- {
- // ...
- plugins: [
- new webpack.DefinePlugin({
- 'process.env': {
- NODE_ENV: "'development'",
- RUN_ENV: process.env.RUN_ENV === 'component' ? "'component'" : "''",
- component: process.env.RUN_ENV === 'component' ? JSON.stringify('/' + component) : 'path'
- }
- })
- ]
- // ...
- }
- // 然后再把 `route.js` 的源码改下
- function loadDocs(path) {
- return r => require.ensure([],
- () => r(require(`./docs${process.env.path}.md`))
- );
- }
万事俱备, 赶紧跑起来
> DONE Compiled successfully in 10792ms
不错不错, 只需要 10s 了, 打开浏览器看看, 也没问题嗯 不错
关掉服务, 试试看原来的 dev 命令是不是也没问题, 嗯, 终端是没问题, 但是浏览器上报错了 ???(黑人问号脸)
好像是 webpack 不能正常的处理, 最后是改成了下面这样才能正常工作
- function loadDocs(path) {
- return r => require.ensure([],
- () => {
- if (process.env.RUN_ENV === 'component') {
- r(require(`./docs${process.env.component}.md`));
- } else {
- r(require(`./docs${path}.md`));
- }
- }
- );
- }
另外, 除了 md 文件只需要处理一个组件的之外, 组件源码也有很多是不需要处理的, 所以, 继续修改下代码
应用的入口处将全局引入 ui 库的方式换成按需
- // 原来的代码
- import Vue from 'vue'
- import gsui from 'components'
- // ...
- Vue.use(gsui)
- // 修改后的
- import Vue from 'vue'
- // ...
- if (process.env.RUN_ENV === 'component') {
- // 一些页面共用的组件
- // 只能用 require 不能 import 因为是静态处理
- Vue.use(require(`components/submenu`).default);
- Vue.use(require(`components/menu`).default);
- Vue.use(require(`components/layout`).default);
- Vue.use(require(`components/menu-item`).default);
- Vue.use(require(`components/header`).default);
- Vue.use(require(`components/icon`).default);
- Vue.use(require(`components/tooltip`).default);
- Vue.use(require(`components/modal`).default);
- Vue.use(require(`components/message`).default);
- Vue.use(require(`components${process.env.component}`).default);
- } else {
- // 不是单组件开发模式引入全部
- Vue.use(require('components').default);
- }
优化后的单组件开发模式和全局开发模式的对比
但是很快就感到不实用, 因为有很多组件是需要依赖其他组件的, 有时候需要看其他组件的文档, 单组件模式就没法做到了
只能再找别的手段了
意外发现, 原来是 vue-loader 的版本原因带来的性能消耗
前天也不知道在哪发现了一个 UI 库 at-ui, 下意识点进去看了下他们的构建配置, 发现和我们的很像(其实 webpack 配置也都差不多的), 也是用了
vue-markdown-loader
, 出于好奇, clone 了下来本地构建了一下结果出乎意料, 他们的构建只需要 16s 16s 16s 怎么会差这么多, 看了下他们的文档, 还是中文和英文双份的 (我们的组件库暂时没有英文文档), 虽然组件没有我们的多, 但是文档绝对是多几十个的, 而且耗时不也是在 md 文件的解析上吗(再次问号脸) 再仔细看了他们的配置和对 md 文件的处理, 确实对 md 文件的处理代码会少很多, 但是这是因为支持的写法不同, 而且也不至于导致时间相差那么多
找不出原因, 干脆用他们的配置来构建我们的项目试试看吧把 build 目录完全 copy 了过来, 修改了一点配置如 entry alias, 安装一些这边不存在的依赖, 其他基本都不需要动了, 总之跑起来看看
磕磕碰碰修改几个报错问题后, 跑起来了, 但是时间还是没变(37s), 奇了怪了再试试另一种, 用我们的配置去跑他们的项目看看
把他们项目的 src 和 docs 目录 copy 了过来, 同样把我们的配置修改一些配置 entry alias 再加点 loader, 他们需要处理 yml 文件, 跑起来看看结果更纳闷了, 时间是 40s(再次问号脸)最后在我们的项目中, 用他们的配置去跑他们的项目, 我这想验证一件事, 会不会是某个依赖的版本不同引起的, 结果确实是这么回事...
接下来就是找出是哪个依赖带来的了, 这里需要注意一点 package.json 中依赖的版本 如 ^1.0.0, 以 ^ 开头的依赖, 安装时总是会按照这个大版本下的最新版本的 也就是 ^1.0.0 ^1.1.0 都是装 1.x 下的最新版本而 ^1.0.0 和 ^2.0.0 才是不一样的最后主要尝试的几个不同版本依赖主要有 webpack(2.x 和 3.x)
vue-markdown-loader
(1.x 和 2.x), 但是这两个换掉之后还是很慢, 最后在同事的提醒下, 可能是 vue-loader 因为
vue-markdown-loader
是依赖 vue-loader 的, 而且无论是 1.x 还是 2.x 都是用的 vue-loader 12.x 的版本, 而我们用的是 13.x 最后功夫不负有心人, 是从 vue-loader 的 v13.1.0 开始, 构建速度会变慢
变慢的原因
下面这个结果是公司的一位牛人发现的
最后发现是 v13.1.0 以上的 vue-loader 采用 prettier 来格式代码, 替代了原来的 js-beautify, 是这个导致了性能问题
最后的配置
- // config.base.js
- module.exports = {
- module: {
- rules: [
- {
- test: /\.css$/,
- use: [
- 'style-loader',
- 'css-loader',
- {
- loader: 'postcss-loader',
- options: {
- config: {
- path: utils.resolve('./postcss.config.js')
- }
- }
- }
- ]
- },
- {
- test: /\.md$/,
- loader: 'vue-markdown-loader',
- options: {
- use: [
- utils.mdAnchor,
- utils.demoContainer,
- utils.tipContainer
- ],
- preprocess: utils.mdPreprocess
- }
- },
- {
- test: /\.scss$/,
- use: 'happypack/loader?id=scss'
- },
- {
- test: /\.jsx?$/,
- exclude: exclude: [/node_modules/, /^dll$/],
- use: 'happypack/loader?id=babel',
- include: [utils.resolve('./components'), utils.resolve('./examples'), utils.resolve('./test')]
- },
- {
- test: /\.json$/,
- loader: 'json-loader'
- },
- {
- test: /\.(jpg|png|gif|eot|svg|ttf|woff|woff2)(\?.*)?(#.*)?$/,
- loader: 'url-loader?name=[name].[hash].[ext]'
- },
- {
- test: /\.vue$/,
- // use: 'happypack/loader?id=vue'
- loader: 'vue-loader',
- options: {
- loaders: {
- css: 'style-loader!css-loader!sass-loader',
- js: 'happypack/loader?id=babel'
- }
- }
- }
- ]
- },
- resolve: {
- extensions: ['.js', '.vue', '.json', '.scss', '.css'],
- alias: {
- 'gs-ui': utils.resolve('./'),
- components: utils.resolve('./components'),
- examples: utils.resolve('./examples')
- }
- },
- plugins: [
- new HappyPack({
- id: 'babel',
- threads: 4,
- loaders: ['babel-loader']
- }),
- new HappyPack({
- id: 'scss',
- threads: 4,
- loaders: [
- 'style-loader',
- 'css-loader',
- {
- loader: 'postcss-loader',
- options: {
- config: {
- path: utils.resolve('./postcss.config.js')
- }
- }
- },
- 'sass-loader'
- ]
- })
- ]
- };
- // config.dev.js
- module.exports = merge(config, {
- entry: entry,
- output: {
- path: '/',
- publicPath: '',
- filename: '[name].js'
- },
- plugins: [
- new webpack.DefinePlugin({
- 'process.env': {
- NODE_ENV: "'development'",
- RUN_ENV: process.env.RUN_ENV === 'component' ? "'component'" : "''",
- component: process.env.RUN_ENV === 'component' ? JSON.stringify('/' + component) : 'path'
- }
- }),
- new HtmlWebpackPlugin({
- template: utils.resolve('examples/index.html'),
- filename: 'index.html',
- inject: true
- }),
- new FriendlyErrorsPlugin(),
- new OpenBrowserPlugin({
- url: 'http://localhost:' + PORT
- }),
- new webpack.DllReferencePlugin({
- manifest: require('./dll/vendor.manifest.json')
- })
- ],
- devServer: {
- disableHostCheck: true,
- host: '0.0.0.0',
- port: PORT,
- quiet: true,
- hot: true,
- historyApiFallback: true
- },
- devtool: 'cheap-eval-source-map'
- });
最后优化后的构建速度 16-17s, 结果还是比较理想的,
结语
最后发现是依赖版本带来的构建性能问题, 不能算是 webpack 构建的优化算是一个踩坑吧
其他在项目中可以用到的优化点应该主要就是 happypack dll 了, 能够有效的提升构建速度, 其他还需要多多尝试
来源: https://juejin.im/post/5a922e776fb9a06337575031