前言
优化前端性能指的是生成环境的优化;提高构建速度指的是开发环境中构建速度的优化;
1、优化前端性能
(1)通过CommonsChunkPlugin将打包后的公共代码抽离成单独文件,减少相同资源被重复加载。
(2)路由中通过import导入组件,实现路由按需加载
(3)通过import实现预加载模块
(4)通过webpack-bundle-analyzer 插件可视化分析webpack的打包结果
2、提高构建速度
(1)优化Loader配置:通过cacheDirectory开启缓存、通过include确定被处理的文件
(2)优化resolve的相关配置:通过resolve.module指明第三方模块的存放位置、减少resolve.extensions的免后缀配置、通过resolve.noParse配置忽略非模块化标准的第三方库以提高构建性能。
一、缩小文件的搜索范围
1、优化Loader配置
由于Loader对文件的转换操作很耗时,所以需要让尽可能少的文件被Loader处理。我们可以通过以下3方面优化Loader配置:
(1)优化正则匹配
(2)通过cacheDirectory选项开启缓存
(3)通过include、exclude来减少被处理的文件。
实践如下:
1 | // 项目原配置 |
2、优化resolve.modules配置
resolve.modules 用于配置Webpack去哪些目录下寻找第三方模块。resolve.modules的默认值是[node modules],含义是先去当前目录的/node modules目录下去找我们想找的模块,如果没找到,就去上一级目录../node modules中找,再没有就去../ .. /node modules中找,以此类推,这和Node.js的模块寻找机制很相似。当安装的第三方模块都放在项目根目录的./node modules目录下时,就没有必要按照默认的方式去一层层地寻找,可以指明存放第三方模块的绝对路径,以减少寻找。
1 | resolve: { |
3、优化resolve.alias配置
resolve.alias配置项通过别名来将原导入路径映射成一个新的导入路径。
1 | alias: { |
4、优化resolve.extensions配置
在导入语句没带文件后缀时,Webpack 会在自动带上后缀后去尝试询问文件是否存在。默认是:extensions : [‘. js ‘,’. json ’] 。也就是说,当遇到require ( ‘. /data ’)这样的导入语句时,Webpack会先去寻找./data .js 文件,如果该文件不存在,就去寻找./data.json 文件,如果还是找不到就报错。如果这个列表越长,或者正确的后缀越往后,就会造成尝试的次数越多,所以 resolve .extensions 的配置也会影响到构建的性能。
优化措施:
(1) 后缀尝试列表要尽可能小,不要将项目中不可能存在的情况写到后缀尝试列表中。
(2) 频率出现最高的文件后缀要优先放在最前面,以做到尽快退出寻找过程。
(3)在源码中写导入语句时,要尽可能带上后缀,从而可以避免寻找过程。例如在确定的情况下将 require(’. /data ’)写成require(’. /data.json ’),可以结合enforceExtension 和 enforceModuleExtension开启使用来强制开发者遵守这条优化。
5、优化resolve.noParse配置
noParse配置项可以让Webpack忽略对部分没采用模块化的文件的递归解析和处理,这 样做的好处是能提高构建性能。原因是一些库如jQuery、ChartJS 庞大又没有采用模块化标准,让Webpack去解析这些文件既耗时又没有意义。 noParse是可选的配置项,类型需要是RegExp 、[RegExp]、function中的一种。例如,若想要忽略jQuery 、ChartJS ,则优化配置如下:
1 | // 使用正则表达式 |
二、减少冗余代码
babel-plugin-transform-runtime 是Babel官方提供的一个插件,作用是减少冗余的代码 。 Babel在将ES6代码转换成ES5代码时,通常需要一些由ES5编写的辅助函数来完成新语法的实现,例如在转换 class extent 语法时会在转换后的 ES5 代码里注入 extent 辅助函数用于实现继承。babel-plugin-transform-runtime会将相关辅助函数进行替换成导入语句,从而减小babel编译出来的代码的文件大小。
三、使用 HappyPack 进行多进程解析和处理文件
由于有大量文件需要解析和处理,所以构建是文件读写和计算密集型的操作,特别是当文件数量变多后,Webpack构建慢的问题会显得更为严重。运行在 Node.之上的Webpack是单线程模型的,也就是说Webpack需要一个一个地处理任务,不能同时处理多个任务。Happy Pack ( https://github.com/amireh/happypack )就能让Webpack做到这一点,它将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程。
项目中HappyPack使用配置:
1 | (1)HappyPack插件安装: |
四、使用 ParalleIUglifyPlugin多进程压缩代码文件
由于压缩JavaScript 代码时,需要先将代码解析成用 Object 抽象表示的 AST 语法树,再去应用各种规则分析和处理AST ,所以导致这个过程的计算量巨大,耗时非常多。当Webpack有多个JavaScript 文件需要输出和压缩时,原本会使用UglifyJS去一个一个压缩再输出,但是ParallelUglifyPlugin会开启多个子进程,将对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过UglifyJS去压缩代码,但是变成了并行执行。所以 ParallelUglify Plugin能更快地完成对多个文件的压缩工作。
项目中ParallelUglifyPlugin使用配置:
1 | (1)ParallelUglifyPlugin插件安装: |
五、开发环境下Source map和热加载的配置
开发环境设置的目的,是让我们的开发环境变得高效轻松。
1、source map的使用
当 webpack 打包源代码时,可能会很难追踪到 error(错误) 和 warning(警告) 在源代码中的原始位置。例如,如果将三个源文件(a.js, b.js 和 c.js)打包到一个 bundle(bundle.js)中,而其中一个源文件包含一个错误,那么堆栈跟踪就会直接指向到 bundle.js。你可能需要准确地知道错误来自于哪个源文件,所以这种提示这通常不会提供太多帮助。
为了更容易地追踪 error 和 warning,JavaScript 提供了source maps功能,可以将编译后的代码映射回原始源代码。如果一个错误来自于 b.js,source map 就会明确的告诉你。
这为我们提供了一种对应编译文件和源文件的方法,使得编译后的代码可读性更好,更容易调试。在webpack配置文件中配置source maps,需要配置devtool。
1 | module.exports = { |
开发环境推荐: cheap-module-eval-source-map
生产环境推荐: cheap-module-source-map
原因如下:
(1)源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加cheap的基本类型来忽略打包前后的列信息。
(2)不管是开发环境还是正式环境,我们都希望能定位到bug的源代码具体的位置,比如说某个vue文件报错了,我们希望能定位到具体的vue文件,因此我们也需要module配置。
(3)我们需要生成map文件的形式,因此我们需要增加source-map属性。
(4)我们介绍了eval打包代码的时候,知道eval打包后的速度非常快,因为它不生成map文件,但是可以对eval组合使用 eval-source-map使用会将map文件以DataURL的形式存在打包后的js文件中。在正式环境中不要使用 eval-source-map, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。
2、watch mode 观察模式
1 | // package.json |
当你运行npm run watch之后,修改代码并保存文件之后,可以看到webpack自动地重新编译修改后的模块。唯一遗憾的是,这时浏览器并没有自动刷新,下面我们使用webpack-dev-server来实现自动刷新浏览器。
(新版本)使用自动刷新
借助自动化的手段,在监听到本地源码文件发生变化时,自动重新构建出可运行的代码后再控制浏览器刷新。Webpack将这些功能都内置了,并且提供了多种方案供我们选择。
项目中自动刷新的配置:
1 | devServer: { |
相关优化措施:
(1)配置忽略一些不监听的一些文件,如:node_modules。
(2)watchOptions.aggregateTirneout 的值越大性能越好,因为这能降低重新构建的频率。
(3) watchOptions.poll 的值越小越好,因为这能降低检查的频率。
3、使用webpack构建本地服务器:监听代码的修改,并自动刷新显示修改后的结果
webpack提供了一个基于node.js构建的本地开发服务器devserver,可以实现上述需求。不过它是一个单独的组件,在webpack配置前需要对其安装作为项目依赖。
1 | npm install webpack-dev-server --save-dev |
然后修改webpack.config.js配置文件,devserver作为webpack配置选项中的一项,配置参数如下:
–open // 自动打开浏览器
–port 3000 // 访问3000端口
–contentBase src // src做为根路径
–hot // 热更新:局部更新,同时起到浏览器的无刷新浏览(仅对于样式,异步刷新页面有效)
(新版本)开启模块热替换
DevServer 还支持一种叫做模块热替换( Hot Module Replacement )的技术可在不刷新整个网页的情况下做到超灵敏实时预览。原理是在一个源码发生变化时,只需重新编译发生变化的模块,再用新输出的模块替换掉浏览器中对应的老模块 。模块热替换技术在很大程度上提升了开发效率和体验 。
项目中模块热替换的配置:
1 | devServer: { |
六、提取公共代码
如果每个页面的代码都将这些公共的部分包含进去,则会造成以下问题 :
• 相同的资源被重复加载,浪费用户的流量和服务器的成本。
• 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。
如果将多个页面的公共代码抽离成单独的文件,就能优化以上问题 。Webpack内置了专门用于提取多个Chunk中的公共部分的插件CommonsChunkPlugin。
项目中CommonsChunkPlugin的配置:
1 | // 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。 |
此外:
(1)dependOn配置可以提取重复的依赖;
(2)SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk;
七、按需加载代码
通过vue写的单页应用时,可能会有很多的路由引入。当打包构建的时候,javascript包会变得非常大,影响加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。
项目中路由按需加载(懒加载)的配置:
1 | const Foo = () => import('./Foo.vue') |
八、缓存
浏览器对于同一个静态资源会自动进行缓存。所以每次打包的dist文件,我们要想办法对于一些未修改的chunk的name保持不变,以便让浏览器继续加载之前的缓存资源。
1、提取引导模块
bundle 的名称是它内容(通过 hash)的映射。如果我们不做修改,然后再次运行构建,我们以为文件名会保持不变。然而,如果我们真的运行,可能会发现情况并非如此,原因是 webpack 在入口 chunk 中,包含了某些 boilerplate(引导模板),特别是 runtime 和 manifest。那么我们就使用代码分离的方式,将runtime代码分离为一个独立的chunk。
1 | optimization: { |
2、将第三方库提取到单独的chunk中
将第三方库(library)(例如 lodash 或 react)提取到单独的 vendor chunk 文件中,是比较推荐的做法,这是因为,它们很少像本地的源代码那样频繁修改。因此通过实现以上步骤,利用 client 的长效缓存机制,命中缓存来消除请求,并减少向 server 获取资源。我们使用 SplitChunksPlugin 插件的 cacheGroups 选项来实现。
1 | optimization: { |
3、moduleIds: ‘deterministic’, 用来解决 bundle 会随着自身的 module.id 的变化,而发生变化。
九、预获取/预加载模块:
十、构建结果输出分析
Webpack输出的代码可读性非常差而且文件非常大,让我们非常头疼。为了更简单、直观地分析输出结果,社区中出现了许多可视化分析工具。这些工具以图形的方式将结果更直观地展示出来,让我们快速了解问题所在。接下来讲解vue项目中用到的分析工具:webpack-bundle-analyzer
项目中在webpack.prod.conf.js进行配置:
1 | if (config.build.bundleAnalyzerReport) { |
执行 $ npm run build –report 后生成分析报告如下: