详解组件库的webpack构建速度优化


Posted in Javascript onJune 18, 2018

背景

在公司的主要工作是组件库(基于vue的ui组件库,类似element-ui)的开发,也已经有两个多月,期间一直觉得项目的开发构建太慢,每次开发打开开发环境需要 40s 左右,简直不能忍。前前后后尝试了各种优化手段,但是都不理想。终于在今天,找到了问题所在,构建速度提升了 50% 以上,现在只需要 17s 左右,整个心情都好了。现在记录一下所用到的各种优化手段,因为是开发环境,所以只考虑构建速度。

各种配置项的优化

主要是对一些loader添加 include exclude之类的小优化,其实这一点并没有带来多少性能的提升,只是一些安慰作用吧。

引入happypack

之前有看到相关文章介绍 happypack 采用多线程处理,能大大提升项目的构建速度。嗯,我觉得这个靠谱。看了下github上的文档,赶紧试试水。

修改webpack一些loader配置,使用happypack

// config.dev.js
{
 // ...
 module: {
  rules: [{
   test: /\.vue$/,
   loader: 'vue-loader',
   options: {
    // 这里估计是都懒得写lang='scss'
    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')
  })
 ]
}

然后记得将打包好的js文件引入,这里可以采用add-asset-html-webpack-plugin

{
 plugins: [
  new AddAssetHtmlPlugin({
   filepath: require.resolve('./dll/vendor.js'),
   includeSourcemap: false
  })
 ]
}

这样,在项目中 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-dev-server没有办法传递参数给process.argv,所以这里采用webpack-hot-middleware

// build/dev-server.js
const webpack = require('webpack');
const webpackConfig = require('./config.dev');
const express = require('express');

webpackConfig.plugins = webpackConfig.plugins || [];

// 全局开发模式采用webpack-dev-server 无需配置hmr,这里需要单独给上
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
webpackConfig.entry.push('webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000');

const compiler = webpack(webpackConfig);
const hotMiddleware = require('webpack-hot-middleware')(compiler, {
 log: false
});

const devMiddleware = require('webpack-dev-middleware')(compiler, {
 publicPath: webpackConfig.output.publicPath,
 quiet: true,
 logLevel: 'silent'
});

const app = express();
app.use(hotMiddleware);
app.use(devMiddleware);
app.use('/build', express.static('./build'));

app.listen(webpackConfig.devServer.port || 8089, '127.0.0.1', () => {
 console.log('Starting server on http://localhost:8089');
});

然后采用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构建速度优化

好像是 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);
}

优化后的单组件开发模式和全局开发模式的对比

详解组件库的webpack构建速度优化

详解组件库的webpack构建速度优化

但是很快就感到不实用,因为有很多组件是需要依赖其他组件的,有时候需要看其他组件的文档,单组件模式就没法做到了

只能再找别的手段了

意外发现,原来是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构建速度优化

结语

最后发现是依赖版本带来的构建性能问题,不能算是webpack构建的优化。算是一个踩坑吧

其他在项目中可以用到的优化点应该主要就是 happypack dll了,能够有效的提升构建速度,其他还需要多多尝试

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
不用构造函数(Constructor)new关键字也能实现JavaScript的面向对象
Jan 11 Javascript
30分钟就入门的正则表达式基础教程
Feb 25 Javascript
在浏览器中实现图片粘贴的jQuery插件-- pasteimg使用指南
Dec 29 Javascript
javascript中innerText和innerHTML属性用法实例分析
May 13 Javascript
js实现新年倒计时效果
Dec 10 Javascript
理解Javascript的call、apply
Dec 16 Javascript
AngularJS的依赖注入实例分析(使用module和injector)
Jan 19 Javascript
基于jquery实现多级菜单效果
Jul 25 jQuery
详解Vue 动态组件与全局事件绑定总结
Nov 11 Javascript
vue使用video.js进行视频播放功能
Jul 18 Javascript
js实现无限瀑布流实例方法
Sep 16 Javascript
Element-UI+Vue模式使用总结
Jan 02 Javascript
Javascript实现异步编程的过程
Jun 18 #Javascript
详解JS函数stack size计算方法
Jun 18 #Javascript
jQuery使用动画队列自定义动画操作示例
Jun 16 #jQuery
node.js自动上传ftp的脚本分享
Jun 16 #Javascript
Vue中props的使用详解
Jun 15 #Javascript
基于jQuery实现的设置文本区域的光标位置
Jun 15 #jQuery
深入浅析Vue全局组件与局部组件的区别
Jun 15 #Javascript
You might like
PHP 巧用数组降低程序的时间复杂度
2010/01/01 PHP
注册页面之前先验证用户名是否存在的php代码
2012/07/14 PHP
php读取csv文件并输出的方法
2015/03/14 PHP
php获取系统变量方法小结
2015/05/29 PHP
php实现可运算的验证码
2015/11/10 PHP
jquery tab标签页的制作
2010/05/10 Javascript
手机平板等移动端适配跳转URL的js代码
2014/01/25 Javascript
浅谈jQuery页面的滚动位置scrollTop、scrollLeft
2015/05/19 Javascript
jQuery无刷新切换主题皮肤实例讲解
2015/10/21 Javascript
详解JSON1:使用TSQL查询数据和更新JSON数据
2016/11/21 Javascript
详解JavaScript RegExp对象
2017/02/04 Javascript
js Date()日期函数浏览器兼容问题解决方法
2017/09/12 Javascript
p5.js入门教程之平滑过渡(Easing)
2018/03/16 Javascript
微信开发之企业付款到银行卡接口开发的示例代码
2018/09/18 Javascript
vue form表单post请求结合Servlet实现文件上传功能
2021/01/22 Vue.js
[01:14:05]《加油DOTA》第四期
2014/08/25 DOTA
[01:30:55]VG vs Mineski Supermajor 败者组 BO3 第三场 6.6
2018/06/07 DOTA
python fabric实现远程部署
2017/01/05 Python
python中的decorator的作用详解
2018/07/26 Python
OpenCV-Python 摄像头实时检测人脸代码实例
2019/04/30 Python
python实现自动化上线脚本的示例
2019/07/01 Python
Python的垃圾回收机制详解
2019/08/28 Python
plt.figure()参数使用详解及运行演示
2021/01/08 Python
HTML5 canvas基本绘图之绘制矩形
2016/06/27 HTML / CSS
amaze ui 的使用详细教程
2020/08/19 HTML / CSS
野兽派官方旗舰店:THE BEAST 野兽派
2016/08/05 全球购物
新加坡最佳婴儿用品店:Mamahood.com.sg
2018/08/26 全球购物
一个精品风格的世界:Atterley
2019/05/01 全球购物
小学清明节活动总结
2014/07/04 职场文书
给女朋友道歉的话大全
2015/01/20 职场文书
教师求职简历自我评价
2015/03/10 职场文书
难以忽视的真相观后感
2015/06/05 职场文书
2019年描写人生经典诗句大全
2019/07/08 职场文书
教师实习自我鉴定总结
2019/08/20 职场文书
Python排序算法之插入排序及其优化方案详解
2021/06/11 Python
idea搭建可运行Servlet的Web项目
2021/06/26 Java/Android