Webpack打包实用优化方案

2018-08-10 admin

目前最火的打包工具莫过于 Webpack 了,关于 Webpack 的优化方案,网上有很多文章可以供大家参考。查阅前人的资料,总结自己在项目中遇到的问题,最终得出一些比较实用的优化方案,本文将与大家一一分享。既然是打包优化,那么我们需要时刻关注以下几点:

Webpack3.0 的 Scope Hoisting (作用域提升)

Webpack3.0 最大的一个新功能就是 ScopeHoisting (作用域提升)。曾经的 Webpack 是将每一个模块(每一个被 import 或者 require 的代码) bundle 到每一个独立的闭包函数里。这会导致 bundle 的文件里,每一个模块外层都会有一些特殊的闭包包装,导致文件增大,同时它也使得编译后的 js 文件在浏览器中的执行效率降低。

虽然,理论知识已经了解,但我们还是用实际操作来验证一下:

开启 ScopeHoisting 非常简单,因为 webpack 已经内置了这个功能,在插件入口处新增 new webpack.optimize.ModuleConcatenationPlugin()。

plugins:[        new webpack.optimize.ModuleConcatenationPlugin()    ]

我们简单的编写两个文件 app.jstimer.js


                                    import
                                         timer from 
                                            './timer'

                                    var
                                         time = 
                                            (new 
                                                Date()).getTime
                                                    ();

                                    timer
                                        ();

                                    console
                                        .log(
                                            'hello webpack'+time
                                                );

export default function bar(){    console.log('pig');}
var path = require('path');var webpack = require('webpack');var config = {    entry:{        app:'./src/app.js'    },     output:{        filename:'bundle.js',        path: path.resolve(__dirname,'./build')    },    plugins:[        //new webpack.optimize.ModuleConcatenationPlugin()    ]}module.exports = config;

执行编译后,结果如下: bundle.js 大小是3.03KB

我们把 new webpack.optimize.ModuleConcatenationPlugin() 打开,编译结果如下:

对比两个的区别大家可以发现,在 bundle.jsWebpack2.0Webpack3.0 多了以下的代码:

(function(module, __webpack_exports__, __webpack_require__) {...});

timer.jsapp.js 都在一个函数中了, timer.js 并没有编译在闭包函数中。一个模块就少了一个闭包函数,那么多引用几个,就可以少很多了。从体积上的确可以看出明显减少。除了这一效果之外,这个内置优化能使得编译后的代码在浏览器中执行效率显著提高。在 Aaron Hardy 的 Optimizing Javascript Through Scope Hoisting[1]的文章中表明,在一个真实的 Tubine[2] 的项目中,作者对比了使用 ScopeHoisting 和不使用的打包大小和js在浏览器执行效率。 Turbine 压缩的gzip文件大小减少了约41%,提高了初始化执行时间约12%。 看到这里,是不是很心动,如果能用在我们项目中,那很完美了。可惜现实比较残酷,下面我们看看它的局限性。 我将 demo 例子的引用模块改成 CommonJs 的语法,执行效果如下:

//import bar from './timer'var bar = require('./timer');var time = (new Date()).getTime();bar();console.log('hello webpack'+time);
// export default function bar(){//     console.log('pig');// }exports.bar = function(){    console.log('big');}

你会发现用 CommonJS 的模块语法在开启 ScopeHoisting 时候,编译打包的 bundle 并没有发生改变。因为目前 webpack3.0 只支持 ESModule 的模块语法。联想一下,你在自己的脚手架中,大多的NPM依赖包都还是 CommonJS的语法, Webpack 会回退到原打包模式。你在做升级处理的时候,可以使用 --display-optimization-bailout 查看被降级原因。

除了目前支持 ESModlue 的语法外,也许你的老代码中还有以下几点:

  • 使用了ProvidePlugin[3]

  • 使用了eva()函数

  • 项目有多个entry

就目前前端生态环境来看, Webpack3.0ScopeHoisting 的新特性暂时无法使用,不过 ESModule 是趋势,未来模块的引用的方式肯定被ES Module所取代。

CommonsChunkPlugin 的使用

Webpack3.0ScopeHoisting 在实际项目中用不了,那我们来看看 CommonsChunkPlugin 这个插件的使用。 CommonChunkPlugin 插件是一个可选的用于建立一个独立文件(又称作 chunk )的功能,这个文件包含多个入口 Chunk 的公共模块( CommonsChunkPlugin 已经从 webpack v4 legato 中移除了,想要了解最新版本中如何处理 chunk ,可以查看 SplitChunksPlugin [4]。 CommonsChunkPlugin 优化的思路就是通过将公共模块拆出来,最终合成的文件能在最开始的是加载一次,便于后续访问其余页面,直接使用浏览器缓存中的公共代码,这样无疑体验会更好。 理论知识知道了,那么我们就动手来试试,是不是效果不错。我们搭一个基于 Webpack2.7.0 的简单的 Vue 脚手架:

const path = require('path');const webpack = require('webpack');const configw = require('./package.json');const HtmlWebpackPlugin = require('html-webpack-plugin');const ExtractTextPlugin = require('extract-text-webpack-plugin');const CleanWebpackPlugin = require('clean-webpack-plugin');const { VueLoaderPlugin } = require('vue-loader')var config = {    entry:{        app:'./src/app.js'    },     output:{        path: path.resolve(__dirname, 'build'),        publicPath: configw.publicPath + '/',        filename: 'js/[name].[chunkhash].js'    },    plugins:[        new CleanWebpackPlugin('build'),        new HtmlWebpackPlugin({            template: './src/index.html'        }),        new ExtractTextPlugin({            filename: 'css/app.css'        }),+        new webpack.optimize.CommonsChunkPlugin({+            name:'vender',+            minChunks: function(module) {+            return (+              module.resource &&+              /\.js$/.test(module.resource) &&+              module.resource.indexOf(+                path.join(__dirname, './node_modules')+              ) === 0+            )+          }++        }),        new VueLoaderPlugin(),    ],    module:{        rules: [                {                test: /\.css$/,                use: ExtractTextPlugin.extract({                    fallback: "style-loader",                    use: ['css-loader']                }),            },            {                test: /\.scss$/,                use: ExtractTextPlugin.extract({                    fallback: 'style-loader',                    use: ['css-loader', 'sass-loader']                })            },            {                test: /\.vue$/,                loader: 'vue-loader',                options: {                }            },            {                test: /\.js$/,                loader: 'babel-loader',                exclude: /node_modules/,                query: {                   presets: ['env']                }              }         ]    }}module.exports = config;

为了方便查看,并没有引入压缩插件什么的做优化。目前来看,在业务js中的依赖于 node_modules 中的 vuevue-routeraxios 等第三方公共库都被抽离出 app.js 打包在 vender.js 中。为了能做到业务js和第三方库js 相分离,做到浏览器端缓存不会频繁更新的 js 迈出了第一步。可惜的是,如果我们改动业务代码比如, app.jsapp.vueindex.vue等业务代码,你会发现除了 app.js 的哈希值发生了变化,连没有做更改的 vender.js 哈希值都变了。

哈希值变了,就说明文件的内容变了。这一定会让你很奔溃,你想做到的 Webpack 构建持久化缓存 js 的功能基本是不可能的。找一下原因吧,没有更改依赖文件,为什么只改动业务 js, vender.js 也会变呢?因为每次构建时候, Webpack 会生成 webpack runtime 代码,用来帮助 Webpack 完成其工作,比如在模块交互时,链接模块所需的加载和解析逻辑。如下图所示,在我们的脚手架中,编译后的结果对比,就只有一个运行时产生的哈希值的值不一样。

既然差别这么小,那么我们把 runtime 这部分代码抽离出来。这样就能持久化缓存 vender.js 了。我们试一下。

new webpack.optimize.CommonsChunkPlugin({            name:'vender',            minChunks: function(module) {            return (              module.resource &&              /\.js$/.test(module.resource) &&              module.resource.indexOf(                path.join(__dirname, './node_modules')              ) === 0            )          }        }),        new webpack.optimize.CommonsChunkPlugin({            name:'manifest',            minChunks:Infinity        })

修改了以下 app.vue 中的代码,前后的编译结果如下:

vender.js 的哈希值如预期一样,没有发生变化。因为文件内变化的代码已经被抽离出到 manifest 这个文件中了。 manifest 里面存储了 chunks映射关系,有了 chunks的映射,我们才知道要加载的 chunk的真实地址。那么每次修改业务 js,都不需要部署 vender.js 了。这样就达到了第三依赖库实现了用户端和服务端持久缓存。每次上线更新也就部署较小的 app.jsmanifest.js 。不过需要注意的是 manifest 必须先加载。 那么直接在生产环境使用这种部署策略,不,还不行。我们的目标只有一个实现第三方库的在持久化缓存,但不能给我们的上线带来风险。要保证你不上线第三方库,用户直接访问本地浏览器缓存中的第三方库,即使上线更新业务 js 依然可以正常运行。 CommonsChunkPlugin 这种打包方式是在运行时编译出的代码,如果我们在业务代码里新增或者删除依赖,试想一下,你项目采用此方式上线后,你对项目也许做优化或者功能模块增加,业务 js 中的对模块依赖部分的代码难免有变动。我们来对比一下保留 index.vue 中的依赖和删除依赖的结果:

很明显我修改了业务代码中的模块依赖,导致了 vender.js 库也发生了变化,这是没法避免的。因为 vender.jsapp.js 是紧密耦合在一块的,你虽然把 runtime 的代码抽离到 manifest 中。对引入模块的删除和新增会导致在运行时编译的模块的id依赖发生变化。我对比了两个 vender.js 的区别,如下图所示,主要是引用的模块 id 发生了变化。

看到这里, CommonsChunkPlugin 虽然能解决问题,但是上线风险无法避免。这样做是不值得,为了利用浏览器缓存,从而只上线 app.js 文件。很难保证上线无问题。最主要问题是,我们在 build 完提交测试还是把 vender.js 一并提交的。当然有人会说你看哈希值不就知道需不需要上线 verder.js 了。可是我上面提到的场景在业务代码中的修改还是很常见的,不值得再上线一次 vender.js。 说了这么多,就是为了告诉我们都不行! vender.js 实现持久化缓存。

DllPlugin 和 DllReferencePlugin

这两个 Webpack 打包插件, Dllplugin 会打包出一个dll文件和一个 manifest.json 模块引用的映射文件。dll文件放什么呢,是我们的第三方库依赖。这样就好比是 Windows 的动态链接库。 Dllplugin 的使用思路是将我们项目中的公共的第三方库打包到一个dll文件中。它是静态的,除非你手动修改在项目中需要引入的库。同时也会编译出一个 manifest.json 的映射文件。它也是静态的,里面存储了通过id值映射值找到在dll文件中对应的库 js。 DllReferencePlugin 则是将映射值打包进我们的业务 js 了。这样就可以完完全全的提前抽离了第三方依赖库。之后,只会打包编译业务部分的代码,再也不用去重复构建第三方库 js。构建编译的时间会大大减少。

我们还是通过实践的方式来证明吧:

首先我们配置一份生成dll的 config

const path = require("path");const webpack = require("webpack");const UglifyJsPlugin = require('uglifyjs-webpack-plugin');const config = require('./package.json');const curDate = new Date();const curTime = curDate.getFullYear() + '/' + (curDate.getMonth() + 1) + '/' + curDate.getDate() + ' ' + curDate.getHours() + ':' + curDate.getMinutes() + ':' + curDate.getSeconds()const bannerTxt = config.name + ' ' + config.version + ' ' + curTime;module.exports = {    //你想要打包的模块数组    entry:{        vendor:['vue','axios','vue-router','qs']    },    output:{        path:path.join(__dirname,'/static/'),        filename:'[name].dll.js',        library:'[name]_library'        //vendor.dll.js 中暴露出的全局变量        //主要是给DllPlugin中的name 使用        //故这里需要和webpack.DllPlugin 中的 'name :[name]_libray 保持一致    },    plugins:[+        new webpack.DllPlugin({+            path:path.join(__dirname,'.','[name]-manifest.json'),+            name:'[name]_library',+            context:__dirname+        }),        new UglifyJsPlugin({            cache:true,            sourceMap:false,            parallel:4,            uglifyOptions: {                ecma:8,                warnings:false,                compress:{                    drop_console:true,                },                output:{                    comments:false,                    beautify:false,                }            }        }),        new webpack.BannerPlugin(bannerTxt)    ]}

entry 配置了常见的 Vue 全家桶系列。因为几乎每个页面都需要用到它们,把它们提到公共的 vender.js 中是再好不过的事了。我们看一下运行结果。我配置了 npm script 执行代码 npm run dll:

"scripts": {    "dev": "webpack-dev-server -d --open --progress",    "build": "cross-env NODE_ENV=production webpack --hide-modules --progress",    "upload": "cross-env NODE_ENV=upload webpack --hide-modules --progress",    "dll": "webpack --config ./webpack.dll.config.js"  }

压缩过的 dll.js 的大小还是可以接受的。我们看看生成的 manifest.json 里面都存储了什么。

跟预期的一样,里面存储了引用映射路径和对应的id值。 dll.jsmanifest.json 只需要编译一次。之后我们开发业务代码和上线打包都不需要再次编译打包 vender.dll.js 了。我们看一下 webpack.config.js 中如何配置的。

const webpack = require('webpack');const config = require('./package.json');const path = require('path');const HtmlWebpackPlugin = require('html-webpack-plugin');const ExtractTextPlugin = require('extract-text-webpack-plugin');const CopyWebpackPlugin = require('copy-webpack-plugin');const CleanWebpackPlugin = require('clean-webpack-plugin');const UglifyJsPlugin = require('uglifyjs-webpack-plugin');const autoprefixer = require('autoprefixer');const htmlwebpackincludeassetsplugin = require('html-webpack-include-assets-plugin');const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');const webpackConfig = module.exports = {};const isProduction = process.env.NODE_ENV === 'production';const isUpload = process.env.NODE_ENV === 'upload';const curDate = new Date();const curTime = curDate.getFullYear() + '/' + (curDate.getMonth() + 1) + '/' + curDate.getDate() + ' ' + curDate.getHours() + ':' + curDate.getMinutes() + ':' + curDate.getSeconds();const bannerTxt = config.name + ' ' + config.version + ' ' + curTime; //构建出的文件顶部banner(注释)内容webpackConfig.entry = {    app: './src/app.js',};webpackConfig.output = {    path: path.resolve(__dirname, 'build' + '/' + config.version),    publicPath: config.publicPath + '/'+config.version+'/',    filename: 'js/[name].js'};webpackConfig.module = {    rules: [{        test: /\.css$/,        use: ExtractTextPlugin.extract({            fallback: "style-loader",            use: ['css-loader', 'postcss-loader']        }),    }, {        test: /\.scss$/,        use: ExtractTextPlugin.extract({            fallback: 'style-loader',            use: ['css-loader', 'sass-loader', 'postcss-loader']        })    }, {        test: /\.vue$/,        loader: 'vue-loader',        options: {            extractCSS: true,            postcss: [require('autoprefixer')()]        }    }, {        test: /\.js$/,        loader: 'babel-loader',        exclude: /node_modules/,    }, {        test: /\.(png|jpg|gif|webp)$/,        loader: 'url-loader',        options: {            limit: 3000,            name: 'img/[name].[ext]',        }    }, ]};webpackConfig.plugins = [    new webpack.optimize.ModuleConcatenationPlugin(),    new CleanWebpackPlugin('build'),    new HtmlWebpackPlugin({        template: './src/index.html'    }),    new ExtractTextPlugin({        filename: 'css/app.css'    }),    new CopyWebpackPlugin([        { from: path.join(__dirname, "./static/"), to: path.join(__dirname, "./build/lib") }    ]),+    new webpack.DllReferencePlugin({+        context:__dirname,+        manifest:require('./vendor-manifest.json')+    })];if (isProduction || isUpload) {    webpackConfig.plugins = (webpackConfig.plugins || []).concat([        new webpack.DefinePlugin({            'process.env': {                NODE_ENV: '"production"'            }        }),        new webpack.LoaderOptionsPlugin({            minimize: true        }),        new UglifyJsPlugin({            cache:true,            sourceMap:false,            parallel:4,            uglifyOptions: {                ecma:8,                warnings:false,                compress:{                    drop_console:true,                },                output:{                    comments:false,                    beautify:false,                }            }        }),        new htmlwebpackincludeassetsplugin({            assets:['/lib/vendor.dll.js'],            publicPath:config.publicPath,            append:false        }),        new webpack.BannerPlugin(bannerTxt)    ]);} else {    webpackConfig.output.publicPath = '/';    webpackConfig.devtool = '#cheap-module-eval-source-map';    webpackConfig.plugins = (webpackConfig.plugins || []).concat([         new AddAssetHtmlPlugin({            filepath:require.resolve('./static/vendor.dll.js'),            includeSourcemap:false,        })    ]);    webpackConfig.devServer = {        contentBase: path.resolve(__dirname, 'build'),        compress: true, //gzip压缩        historyApiFallback: true,    };}

我们用 DllReferencePlugin 把生成好的 manifest.json 映射文件引入到正式的业务代码打包中。

app.js 只有7.45KB, vender.dll.js 被拷贝到 build 目录下 lib 文件夹下。

所有业务的代码则都在版本控制文件夹下, vender.dll.js 放置在 lib 文件夹下。每次上线如果有版本变化只要上线业务js就行。不需要上线 lib 文件夹。只要你不手动修改 webpack.dll.config.js 的entry

entry:{        vendor:['vue','axios','vue-router','qs']    }

就永远不会发生变化。 还是对比一样优化之前和优化之后的:

优化之前 app.js 由于有打包了第三方库所以有116KB,优化之后,抽离第三库 app.js 只有7.45KB。只是项目开始时候,需要提前打包一份dll文件。以后每次编译时间都比之前少了将近一半。这个还只是一个脚手架demo。等到用在实际项目中的构建,效果更加明显。

总结

有很多人不建议使用 DllPlugin ,觉得没必要把所有公共的打包在一起,放在首屏就加载,这样使得首屏加载时间过长之类的,还有觉得多了一份 config 增加了工作量。不过我个人觉得,对于像 ReactVue 这种全家桶系列的,整体性偏强的技术栈。抽离出全家桶放置在 vender.js 中还是很有必要的。因为几乎每个页面都会用到。而且,他们是完全跟业务逻辑无关的第三方库。对它们实现持久化缓存,对于开发者和用户的体验都会大大提升。一点脚手架搭建的心得,感谢各位的浏览,有任何问题欢迎讨论哈~

扩展阅读:

[1]ptimizing Javascript Through Scope Hoisting:https://medium.com/launch-by-adobe/optimizing-javascript-through-scope-hoisting-47c132ef27e4 [2]Tubine:https://github.com/Adobe-Marketing-Cloud/reactor-turbine [3]ProvidePlugin:https://webpack.js.org/plugins/provide-plugin/ [4]SplitChunksPlugin:https://webpack.js.org/plugins/split-chunks-plugin

原文链接:https://mp.weixin.qq.com/s?__biz=MzUxMDYxNTgwMA==&mid=2247484062&idx=1&sn=dce6b7e7ddaeb9eef683a226fd833c1f&chksm=f9010b09ce76821f059c59aba873f043cd5cc824221188e307983f02a83c0fffa8bea14c833b#rd

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-37676.html

文章标题:Webpack打包实用优化方案

相关文章
[翻译]基于Webpack4使用懒加载分离打包React代码
原文地址:https://engineering.innovid.com/code-splitting-using-lazy-loading-with-react-redux-typescript-and-webpack-4-3ec601...
2018-03-11
javascript中CheckBox全选终极方案
在我们的程序开发中经常会要用到CheckBox的全选,通常情况下是在一些数据绑定控件中如Gridview等 下面以Repeater为例,在Repeater的header 和item中放入CheckBox控件 。 <asp:Repeat...
2017-03-23
JavaScript里实用的原生API汇总
直接进入正题 解析字符串对象 我们都知道,JavaScript对象可以序列化为JSON,JSON也可以解析成对象,但是问题是如果出现了一个既不是JSON也不是对象的"东西",转成哪一方都不方便,那么eval就可以派上用场...
2017-03-23
webpack4 css打包压缩问题
这两天一直在练习这个webpack4, 发现有好多问题和坑,做开发嘛,一定要有喜欢出问题并喜欢解决问题,坚决踩个坑填个坑的不怕死小强精神! webpack4 在配置上其实是可以是想production和development的, &#x2F...
2018-05-18
使用RequireJS优化JavaScript引用代码的方法
RequireJS是一个提高你的javascript代码速度和质量的有效方法,同时它还让你的代码更容易阅读和维护。 在本文中,我会为你介绍RequireJS和应该如何使用它。我们讨论引入文件和定义模块,甚至还会接触优化方面的知识。 简单的说...
2017-03-27
鼠标经过子元素触发mouseout,mouseover事件的解决方案
我想实现的目标:当鼠标进入黑色框时,橙色框执行淡入动画;当黑色框范围移动的时候(即使经过粉色框,动画仍然不被触发);当鼠标移出的时候,橙色方块消失。 遇到的问题阐述:当鼠标移入黑色框的时候,橙色框执行淡入动画,但是当鼠标从黑色框经过粉色框的...
2017-03-27
Vue.js中用webpack合并打包多个组件并实现按需加载
前言 随着移动设备的升级、网络速度的提高,用户对于web应用的要求越来越高,web应用要提供的功能越来越。功能的增加导致的最直观的后果就是资源文件越来越大。为了维护越来越庞大的客户端代码,提出了模块化的概念来组织代码。webpack作为一种...
2017-03-16
webpack入门+react环境配置
本文介绍了react.js使用webpack搭配环境的入门教程,分享给大家,也给自己做个笔记 如果你想直接上手开发,而跳过这些搭配环境的繁琐过程,推荐你使用官方的create-react-app命令 npm install -g creat...
2017-03-20
实现placeholder效果的方案汇总
placeholder是html5<input>的一个属性,它提供可描述输入字段预期值的提示信息(hint), 该提示会在输入字段为空时显示。高端浏览器支持此属性(ie10/11在获得焦点时文字消失),ie6/7/8/9则不支持...
2017-03-24
利用js判断手机是否安装某个app的多种方案
前言 大家在日常开发的时候,经常会遇到这样的需求,通过检测手机,如果本地安装了app那么直接打开,否则苹果要跳转到app-store,安卓则要跳到对应的市场,下面来给大家介绍几种解决的方案。 解决方案 一 //html代...
2017-02-15
回到顶部