文档章节

webpack 构建性能优化策略小结

黑魔法
 黑魔法
发布于 2017/01/23 10:49
字数 3963
阅读 50
收藏 1

背景

  2016年行将结束,回顾这一年来前端技术的发展真的可以用百(gui)花(quan)争(zhen)鸣(luan)来形容,无论是技术栈的演进,技术框架的推新,还是各种模式,反模式的最佳实践都在不断地涌现,网上的一篇文章《在 2016 年学 Java 是一种什么样的体验?》(https://www.v2ex.com/t/310767)更是把这一现状做了很好总结。

  当然,吐槽归吐槽,技术的车轮还是要始终向前迈进,从中我们也不难发现,从前那种直接在 js 中写脚本。通过 src 嵌入到页面,然后按 F5 刷新页面查看结果的开发方式已经渐行渐远,基本上选择一款合适的编译和资源管理工具已经成为了所有前端工程中的标配,而在诸多的构建工具中,webpack 以其丰富的功能和灵活的配置在整个2016年中大放光彩,React,Vue,angularjs2 等诸多知名项目也都选用其作为官方构建工具,极受业内追捧,但是随者工程开发的复杂程度和代码规模不断地增加,webpack 暴露出来的各种性能问题也愈发明显,极大的影响着开发过程中的体验。

  

  问题归纳

  历经了多个 web 项目的实战检验,我们对 webapck 在构建中逐步暴露出来的性能问题归纳主要有如下几个方面:

  • 代码全量构建速度过慢,即使是很小的改动,也要等待长时间才能查看到更新与编译后的结果(引入 HMR 热更新后有明显改进);

  • 随着项目业务的复杂度增加,工程模块的体积也会急剧增大,构建后的模块通常要以 M 为单位计算;

  • 多个项目之间共用基础资源存在重复打包,基础库代码复用率不高;

  • node 的单进程实现在耗 cpu 计算型 loader 中表现不佳;

  针对以上的问题,我们来看看怎样利用 webpack 现有的一些机制和第三方扩展插件来逐个击破。

  慢在何处

  作为工程师,我们一直鼓励要理性思考,用数据和事实说话,“我觉得很慢”,“太卡了”,“太大了”之类的表述难免显得太笼统和太抽象,那么我们不妨从如下几个方面来着手进行分析:

  

  • 从项目结构着手,代码组织是否合理,依赖使用是否合理;

  • 从 webpack 自身提供的优化手段着手,看看哪些 api 未做优化配置;

  • 从 webpack 自身的不足着手,做有针对性的扩展优化,进一步提升效率;

  在这里我们推荐使用一个 wepback 的可视化资源分析工具:webpack-visualizer,在 webpack 构建的时候会自动帮你计算出各个模块在你的项目工程中的依赖与分布情况,方便做更精确的资源依赖和引用的分析。

  从上图中我们不难发现大多数的工程项目中,依赖库的体积永远是大头,通常体积可以占据整个工程项目的7-9成,而且在每次开发过程中也会重新读取和编译对应的依赖资源,这其实是很大的的资源开销浪费,而且对编译结果影响微乎其微,毕竟在实际业务开发中,我们很少会去主动修改第三方库中的源码,改进方案如下:

  方案一、合理配置 CommonsChunkPlugin

  webpack 的资源入口通常是以 entry 为单元进行编译提取,那么当多 entry 共存的时候,CommonsChunkPlugin 的作用就会发挥出来,对所有依赖的 chunk 进行公共部分的提取,但是在这里可能很多人会误认为抽取公共部分指的是能抽取某个代码片段,其实并非如此,它是以 module 为单位进行提取。

  假设我们的页面中存在 entry1,entry2,entry3 三个入口,这些入口中可能都会引用如 utils,loadash,fetch 等这些通用模块,那么就可以考虑对这部分的共用部分机提取。通常提取方式有如下四种实现:

  1、传入字符串参数,由 chunkplugin 自动计算提取

  newwebpack.optimize.CommonsChunkPlugin( 'common.js')

  这种做法默认会把所有入口节点的公共代码提取出来, 生成一个 common.js

  2、有选择的提取公共代码

  newwebpack.optimize.CommonsChunkPlugin( 'common.js',[ 'entry1', 'entry2']);

  只提取 entry1 节点和 entry2 中的共用部分模块, 生成一个 common.js

  3、将 entry 下所有的模块的公共部分(可指定引用次数)提取到一个通用的 chunk 中

  newwebpack.optimize.CommonsChunkPlugin({ name: 'vendors', minChunks: function(module, count){

  return(

  module.resource &&

  /.js$/.test( module.resource) &&

  module.resource.indexOf( path.join(__dirname, '../node_modules') ) === 0) }});

  提取所有 node_modules 中的模块至 vendors 中,也可以指定 minChunks 中的最小引用数;

  4、抽取 enry 中的一些 lib 抽取到 vendors 中

  entry = { vendors: [ 'fetch', 'loadash']};

  newwebpack.optimize.CommonsChunkPlugin({ name: "vendors", minChunks: Infinity

  });

  添加一个 entry 名叫为 vendors,并把 vendors 设置为所需要的资源库,CommonsChunk 会自动提取指定库至 vendors 中。

  方案二、通过 externals 配置来提取常用库

  在实际项目开发过程中,我们并不需要实时调试各种库的源码,这时候就可以考虑使用 external 选项了。

  

  简单来说 external 就是把我们的依赖资源声明为一个外部依赖,然后通过 外链脚本引入。这也是我们早期页面开发中资源引入的一种翻版,只是通过配置后可以告知 webapck 遇到此类变量名时就可以不用解析和编译至模块的内部文件中,而改用从外部变量中读取,这样能极大的提升编译速度,同时也能更好的利用 CDN 来实现缓存。

  external 的配置相对比较简单,只需要完成如下三步:

  1、在页面中加入需要引入的 lib 地址,如下:

  <head>

  <src="//cdn.bootcss.com/jquery.min.js"></>

  <src="//cdn.bootcss.com/underscore.min.js"></>

  <src="/static/common/react.min.js"></>

  <src="/static/common/react-dom.js"></>

  <src="/static/common/react-router.js"></>

  <src="/static/common/immutable.js"></>

  </head>

  2、在 webapck.config.js 中加入 external 配置项:

  module.export = { externals: {

  'react-router': { amd: 'react-router', root: 'ReactRouter', commonjs: 'react-router', commonjs2: 'react-router'}, react: { amd: 'jquery', root: 'jQuery', commonjs: 'jquery', commonjs2: 'jquery'}, react: { amd: 'react', root: 'React', commonjs: 'react', commonjs2: 'react'}, 'react-dom': { amd: 'react-dom', root: 'ReactDOM', commonjs: 'react-dom', commonjs2: 'react-dom'} }}

  这里要提到的一个细节是:此类文件在配置前,构建这些资源包时需要采用amd/commonjs/cmd相关的模块化进行兼容封装,即打包好的库已经是 umd 模式包装过的,如在 node_modules/react-router 中我们可以看到 umd/ReactRouter.js 之类的文件,只有这样 webpack 中的 require 和 import * from ‘xxxx’ 才能正确读到该类包的引用,在这类 js 的头部一般也能看到如下字样:

  if( typeofexports === 'object'&& typeofmodule=== 'object') {

  module.exports = factory( require( "react"));} elseif( typeofdefine === 'function'&& define.amd) { define([ "react"], factory);} elseif( typeofexports === 'object') { exports[ "ReactRouter"] = factory( require( "react"));} else{ root[ "ReactRouter"] = factory(root[ "React"]);}

  3、非常重要的是一定要在 output 选项中加入如下一句话:

  output: { libraryTarget: 'umd'

  }

  由于通过 external 提取过的 js 模块是不会被记录到 webapck 的 chunk 信息中,通过 libraryTarget 可告知我们构建出来的业务模块,当读到了 externals 中的 key 时,需要以 umd 的方式去获取资源名,否则会有出现找不到 module 的情况。

  通过配置后,我们可以看到对应的资源信息已经可以在浏览器的 source map 中读到了。

  

  对应的资源也可以直接由页面外链载入,有效地减小了资源包的体积。

  

  方案三、利用 DllPlugin 和 DllReferencePlugin 预编译资源模块

  我们的项目依赖中通常会引用大量的 npm 包,而这些包在正常的开发过程中并不会进行修改,但是在每一次构建过程中却需要反复的将其解析,如何来规避此类损耗呢?这两个插件就是干这个用的。

  简单来说 DllPlugin 的作用是预先编译一些模块,而 DllReferencePlugin 则是把这些预先编译好的模块引用起来。这边需要注意的是 DllPlugin 必须要在 DllReferencePlugin 执行前先执行一次,dll 这个概念应该也是借鉴了 windows 程序开发中的 dll 文件的设计理念。

  相对于 externals,dllPlugin 有如下几点优势:

  • dll 预编译出来的模块可以作为静态资源链接库可被重复使用,尤其适合多个项目之间的资源共享,如同一个站点 pc 和手机版等;

  • dll 资源能有效地解决资源循环依赖的问题,部分依赖库如:react-addons-css-transition-group 这种原先从 react 核心库中抽取的资源包,整个代码只有一句话

      module.exports = require('react/lib/ReactCSSTransitionGroup');

      却因为重新指向了 react/lib 中,这也会导致在通过 externals 引入的资源只能识别 react,寻址解析 react/lib 则会出现无法被正确索引的情况。

  • 由于 externals 的配置项需要对每个依赖库进行逐个定制,所以每次增加一个组件都需要手动修改,略微繁琐,而通过 dllPlugin 则能完全通过配置读取,减少维护的成本;

  1、配置 dllPlugin 对应资源表并编译文件

  那么 externals 该如何使用呢,其实只需要增加一个配置文件:webpack.dll.config.js:

  constwebpack = require( 'webpack');

  constpath = require( 'path');

  constisDebug = process.env.NODE_ENV === 'development';

  constoutputPath = isDebug ? path.join(__dirname, '../common/debug') : path.join(__dirname, '../common/dist');

  constfileName = '[name].js';

  // 资源依赖包,提前编译

  constlib = [

  'react',

  'react-dom',

  'react-router',

  'history',

  'react-addons-pure-render-mixin',

  'react-addons-css-transition-group',

  'redux',

  'react-redux',

  'react-router-redux',

  'redux-actions',

  'redux-thunk',

  'immutable',

  'whatwg-fetch',

  'byted-people-react-select',

  'byted-people-reqwest'

  ];

  constplugin = [

  newwebpack.DllPlugin({

  /** * path * 定义 manifest 文件生成的位置 * [name]的部分由entry的名字替换 */path: path.join(outputPath, 'manifest.json'),

  /** * name * dll bundle 输出到那个全局变量上 * 和 output.library 一样即可。 */name: '[name]', context: __dirname }),

  newwebpack.optimize.OccurenceOrderPlugin()];

  if(!isDebug) { plugin.push(

  newwebpack.DefinePlugin({

  'process.env.NODE_ENV': JSON.stringify( 'production') }),

  newwebpack.optimize.UglifyJsPlugin({ mangle: { except: [ '$', 'exports', 'require'] }, compress: { warnings: false}, output: { comments: false} }) )}

  module.exports = { devtool: '#source-map', entry: { lib: lib }, output: { path: outputPath, filename: fileName,

  /** * output.library * 将会定义为 window.${output.library} * 在这次的例子中,将会定义为`window.vendor_library` */library: '[name]', libraryTarget: 'umd', umdNamedDefine: true}, plugins: plugin};

  然后执行命令:

  $ NODE_ENV=development webpack --config webpack.dll.lib.js --progress$ NODE_ENV=production webpack --config webpack.dll.lib.js --progress

  即可分别编译出支持调试版和生产环境中 lib 静态资源库,在构建出来的文件中我们也可以看到会自动生成如下资源:

  common├── debug│ ├── lib.js│ ├── lib.js.map│ └── manifest.json└── dist ├── lib.js ├── lib.js.map └── manifest.json

  文件说明:

  • lib.js 可以作为编译好的静态资源文件直接在页面中通过 src 链接引入,与 externals 的资源引入方式一样,生产与开发环境可以通过类似 charles 之类的代理转发工具来做路由替换;

  • manifest.json中保存了 webpack 中的预编译信息,这样等于提前拿到了依赖库中的 chunk 信息,在实际开发过程中就无需要进行重复编译;

  2、dllPlugin 的静态资源引入

  lib.js 和 manifest.json 存在一一对应的关系,所以我们在调用的过程也许遵循这个原则,如当前处于开发阶段,对应我们可以引入 common/debug 文件夹下的 lib.js 和 manifest.json,切换到生产环境的时候则需要引入 common/dist 下的资源进行对应操作,这里考虑到手动切换和维护的成本,我们推荐使用add-asset-html-webpack-plugin进行依赖资源的注入,可得到如下结果:

  <head>

  <src="/static/common/lib.js"></>

  </head>

  在 webpack.config.js 文件中增加如下代码:

  constisDebug = (process.env.NODE_ENV === 'development');

  constlibPath = isDebug ? '../dll/lib/manifest.json': '../dll/dist/lib/manifest.json';

  // 将 mainfest.json 添加到 webpack 的构建中

  module.export = { plugins: [

  newwebpack.DllReferencePlugin({ context: __dirname, manifest: require(libPath), }) ]}

  配置完成后我们能发现对应的资源包已经完成了纯业务模块的提取

  

  多个工程之间如果需要使用共同的 lib 资源,也只需要引入对应的 lib.js 和 manifest.js 即可,plugin 配置中也支持多个 webpack.DllReferencePlugin 同时引入使用,如下:

  module.export = { plugins: [

  newwebpack.DllReferencePlugin({ context: __dirname, manifest: require(libPath), }),

  newwebpack.DllReferencePlugin({ context: __dirname, manifest: require(ChartsPath), }) ]} 方案四、使用 Happypack 加速你的代码构建

  以上介绍均为针对 webpack 中的 chunk 计算和编译内容的优化与改进,对资源的实际体积改进上也较为明显,那么除此之外,我们能否针对资源的编译过程和速度优化上做些尝试呢?

  众所周知,webpack 中为了方便各种资源和类型的加载,设计了以 loader 加载器的形式读取资源,但是受限于 node 的编程模型影响,所有的 loader 虽然以 async 的形式来并发调用,但是还是运行在单个 node 的进程以及在同一个事件循环中,这就直接导致了当我们需要同时读取多个 loader 文件资源时,比如 babel-loader 需要 transform 各种 jsx,es6 的资源文件。在这种同步计算同时需要大量耗费 cpu 运算的过程中,node 的单进程模型就无优势了,那么 happypack 就针对解决此类问题而生。

  开启 happypack 的线程池

  happypack 的处理思路是将原有的 webpack 对 loader 的执行过程从单一进程的形式扩展多进程模式,原本的流程保持不变,这样可以在不修改原有配置的基础上来完成对编译过程的优化,具体配置如下:

  varHappyPack = require( 'happypack');

  varhappyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

  module: { loaders: [ { test: /.js[x]?$/,

  // loader: 'babel-loader?presets[]=es2015&presets[]=react'loader: 'happypack/loader?id=happybabel'} ] }, plugins: [

  newHappyPack({ id: 'happybabel', loaders: [ 'babel-loader'], threadPool: happyThreadPool, cache: true, verbose: true}) ]

  我们可以看到通过在 loader 中配置直接指向 happypack 提供的 loader,对于文件实际匹配的处理 loader,则是通过配置在plugin属性来传递说明,这里 happypack 提供的 loader 与 plugin 的衔接匹配,则是通过 id=happybabel 来完成。配置完成后,laoder 的工作模式就转变成了如下所示:

  

  happypack 在编译过程中除了利用多进程的模式加速编译,还同时开启了 cache 计算,能充分利用缓存读取构建文件,对构建的速度提升也是非常明显的,经过测试,最终的构建速度提升如下:

  优化前:

  

  优化后:

  

  关于 happyoack 的更多介绍可以查看:

  • happypack

  • happypack 原理解析

方案五、增强 uglifyPlugin

  uglifyJS 凭借基于 node 开发,压缩比例高,使用方便等诸多优点已经成为了 js 压缩工具中的首选,但是我们在 webpack 的构建中观察发现,当 webpack build 进度走到 80% 前后时,会发生很长一段时间的停滞,经测试对比发现这一过程正是 uglfiyJS 在对我们的 output 中的 bunlde 部分进行压缩耗时过长导致,针对这块我们推荐使用webpack-uglify-parallel来提升压缩速度。

  从插件源码中可以看到,webpack-uglify-parallel 的是实现原理是采用了多核并行压缩的方式来提升我们的压缩速度。

  plugin.nextWorker().send({ input: input, inputSourceMap: inputSourceMap, file: file, options: options});plugin._queue_len++;

  if(!plugin._queue_len) { callback();} if( this.workers.length < this.maxWorkers) {

  varworker = fork(__dirname + '/lib/worker'); worker.on( 'message', this.onWorkerMessage.bind( this)); worker.on( 'error', this.onWorkerError.bind( this));

  this.workers.push(worker);}

  this._next_worker++;

  returnthis.workers[ this._next_worker % this.maxWorkers];

  使用配置也非常简单,只需要将我们原来 webpack 中自带的 uglifyPlugin 配置:

  newwebpack.optimize.UglifyJsPlugin({ exclude: /.min.js$/mangle: true, compress: { warnings: false}, output: { comments: false}})

  修改成如下代码即可:

  constos = require( 'os');

  constUglifyJsParallelPlugin = require( 'webpack-uglify-parallel');

  newUglifyJsParallelPlugin({ workers: os.cpus().length, mangle: true, compressor: { warnings: false, drop_console: true, drop_debugger: true} }) 适用场景

  在实际的开发过程中,可灵活地选择适合自身业务场景的优化手段。

  

  工程演示demo(https://github.com/taikongfeizhu/webpack-dll-demo)

  小结

  性能优化无小事,追求快没有止境,在前端工程日益庞大复杂的今天,针对实际项目,持续改进构建工具的性能,对项目开发效率的提升和工具深度理解都是极其有益的。

本文转载自:http://mt.sohu.com/20161228/n477217155.shtml

黑魔法
粉丝 15
博文 178
码字总数 44775
作品 0
武汉
前端工程师
私信 提问
【性能优化】webpack前端构建性能优化策略小结

背景 回顾2016的前端技术的发展真的可以用百(gui)花(quan)争(zhen)鸣(luan)来形容,无论是技术栈的演进,技术框架的推新,还是各种模式,反模式的最佳实践都在不断地涌现,网上的一篇...

今日头条技术团队
2018/05/24
0
0
作为全栈老人,为什么我推荐你学好 webpack?

目前为止,webpack 在 GitHub上已拥有 48.8k 的 star,在前端代码打包器领域内,算得上是时下最流行的前端打包工具。它可以分析各个模块的依赖关系,最终打包成我们常见的静态文件:.js 、 ...

dotNET跨平台
05/28
0
0
认识webpack原理-万物皆可打包

Webpack早已成为前端开发者必备技能之一,本文从webpack的原理、打包流程以及webpack优化进行分析。webpack配置的文章在网上已经数不胜数了,本文就不作阐述啦。有问题的小伙伴欢迎留言指正~...

阿拉斯加大狗
09/26
0
0
深入理解webpack的chunkId对线上缓存的思考

前言 想必经常使用基于webpack打包工具的框架的同学们,无论是使用React还是Vue在性能优化上使用最多的应该是分包策略(按需加载)。按需加载的方式使我们的每一个bundle变的更小,在每一个单...

c_Kim
08/26
0
0
React router动态加载组件-适配器模式的应用

前言 本文讲述怎么实现动态加载组件,并借此阐述适配器模式。 一、普通路由例子 以上是最常见的。在简单的单页应用中,这样写是ok的。因为打包后的单一js文件也不过200k左右,之后,对加载性...

我是leon
2018/09/12
0
0

没有更多内容

加载失败,请刷新页面

加载更多

nginx学习笔记

中间件位于客户机/ 服务器的操作系统之上,管理计算机资源和网络通讯。 是连接两个独立应用程序或独立系统的软件。 web请求通过中间件可以直接调用操作系统,也可以经过中间件把请求分发到多...

码农实战
今天
5
0
Spring Security 实战干货:玩转自定义登录

1. 前言 前面的关于 Spring Security 相关的文章只是一个预热。为了接下来更好的实战,如果你错过了请从 Spring Security 实战系列 开始。安全访问的第一步就是认证(Authentication),认证...

码农小胖哥
今天
9
0
JAVA 实现雪花算法生成唯一订单号工具类

import lombok.SneakyThrows;import lombok.extern.slf4j.Slf4j;import java.util.Calendar;/** * Default distributed primary key generator. * * <p> * Use snowflake......

huangkejie
昨天
12
0
PhotoShop 色调:RGB/CMYK 颜色模式

一·、 RGB : 三原色:红绿蓝 1.通道:通道中的红绿蓝通道分别对应的是红绿蓝三种原色(RGB)的显示范围 1.差值模式能模拟三种原色叠加之后的效果 2.添加-颜色曲线:调整图像RGB颜色----R色增强...

东方墨天
昨天
11
1
将博客搬至CSDN

将博客搬至CSDN

算法与编程之美
昨天
13
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部