上一篇文章 Tree-Shaking性能优化实践 - 原理篇 介绍了 tree-shaking 的原理,本文主要介绍 tree-shaking 的实践
css
webpack2 发布,宣布支持tree-shaking,webpack 3发布,支持做用域提高,生成的bundle文件更小。 再没有升级webpack以前,增幻想咱们的性能又要大幅提高了,对升级充满了期待。实际上事实是这样的html
升级完以后,bundle文件大小并无大幅减小,当时有较大的心理落差,而后去研究了为何效果不理想,缘由见 Tree-Shaking性能优化实践 - 原理篇 。前端
优化仍是要继续的,虽然工具自带的tree-shaking不能去除太多无用代码,在去除无用代码这一方面也仍是有能够作的事情。咱们从三个方面作里一些优化。vue
先来看一个问题node
当咱们使用组件库的时候,import {Button} from 'element-ui',相对于Vue.use(elementUI),已是具备性能意识,是比较推荐的作法,但若是咱们写成右边的形式,具体到文件的引用,打包以后的区别是很是大的,以antd为例,右边形式bundle体积减小约80%。react
这个引用也属于有反作用,webpack不能把其余组件进行tree-shaking。既然工具自己是作不了,那咱们能够作工具把左边代码自动改为右边代码这种形式。这个工具antd库自己也是提供的。我在antd的工具基础上作了少许的修改,不用任何配置,原生支持咱们本身的组件库, wui 和 xcui 以及一些其余经常使用的库webpack
babel-plugin-import-fix ,缩小引用范围git
下面介绍一下原理github
这是一个babel的插件,babel经过核心babylon将ES6代码转换成AST抽象语法树,而后插件遍历语法树找出相似import {Button} from 'element-ui'这样的语句,进行转换,最后从新生成代码。web
babel-plugin-import-fix默认支持antd,element,meterial-UI,wui,xcui和d3,只须要再.babelrc中配置插件自己就能够。
.babelrc
{
"presets": [
["es2015", { "modules": false }], "react"
],
"plugins": ["import-fix"]
}
复制代码
实际上是想把全部经常使用的库都默认支持,但不少经常使用的库却不支持缩小引用范围。由于没有独立输出各个子模块,不能把引用修改成对单个子模块的引用。
咱们前面所说的tree-shaking都是针对js文件,经过静态分析,尽量消除无用的代码,那对于css咱们能作tree-shaking吗?
随着CSS3,LESS,SASS等各类css预处理语言的普及,css文件在整个工程中占比是不可忽视的。随着大项目功能的不停迭代,致使css中可能就存在着无用的代码。我实现了一个webpack插件来解决这个问题,找出css代码无用的代码。
webpack-css-treeshaking-plugin,对css进行tree-shaking
下面介绍一下原理
总体思路是这样的,遍历全部的css文件中的selector选择器,而后去全部js代码中匹配,若是选择器没有在代码出现过,则认为该选择器是无用代码。
首先面临的问题是,如何优雅的遍历全部的选择器呢?难道要用正则表达式很苦逼的去匹配分割吗?
babel是js世界的福星,其实css世界也有利器,那就是postCss。
PostCSS 提供了一个解析器,它可以将 CSS 解析成AST抽象语法树。而后咱们能写各类插件,对抽象语法树作处理,最终生成新的css文件,以达到对css进行精确修改的目的。
总体又是一个webpack的插件,架构图以下:
主要流程:
apply (compiler) {
compiler.plugin('after-emit', (compilation, callback) => {
let styleFiles = Object.keys(compilation.assets).filter(asset => {
return /\.css$/.test(asset)
})
let jsFiles = Object.keys(compilation.assets).filter(asset => {
return /\.(js|jsx)$/.test(asset)
})
....
}
复制代码
let tasks = []
styleFiles.forEach((filename) => {
const source = compilation.assets[filename].source()
let listOpts = {
include: '',
source: jsContents, //传入所有js文件
opts: this.options //插件配置选项
}
tasks.push(postcss(treeShakingPlugin(listOpts)).process(source).then(result => {
let css = result.toString() // postCss处理后的css AST
//替换webpack的编译产物compilation
compilation.assets[filename] = {
source: () => css,
size: () => css.length
}
return result
}))
})
复制代码
module.exports = postcss.plugin('list-selectors', function (options) {
// 从根节点开始遍历
cssRoot.walkRules(function (rule) {
// Ignore keyframes, which can log e.g. 10%, 20% as selectors
if (rule.parent.type === 'atrule' && /keyframes/.test(rule.parent.name)) return
// 对每个规则进行处理
checkRule(rule).then(result => {
if (result.selectors.length === 0) {
// 选择器所有被删除
let log = ' ✂️ [' + rule.selector + '] shaked, [1]'
console.log(log)
if (config.remove) {
rule.remove()
}
} else {
// 选择器被部分删除
let shaked = rule.selectors.filter(item => {
return result.selectors.indexOf(item) === -1
})
if (shaked && shaked.length > 0) {
let log = ' ✂️ [' + shaked.join(' ') + '] shaked, [2]'
console.log(log)
}
if (config.remove) {
// 修改AST抽象语法树
rule.selectors = result.selectors
}
}
})
})
复制代码
checkRule 处理每个规则核心代码
let checkRule = (rule) => {
return new Promise(resolve => {
...
let secs = rule.selectors.filter(function (selector) {
let result = true
let processor = parser(function (selectors) {
for (let i = 0, len = selectors.nodes.length; i < len; i++) {
let node = selectors.nodes[i]
if (_.includes(['comment', 'combinator', 'pseudo'], node.type)) continue
for (let j = 0, len2 = node.nodes.length; j < len2; j++) {
let n = node.nodes[j]
if (!notCache[n.value]) {
switch (n.type) {
case 'tag':
// nothing
break
case 'id':
case 'class':
if (!classInJs(n.value)) {
// 调用classInJs判断是否在JS中出现过
notCache[n.value] = true
result = false
break
}
break
default:
// nothing
break
}
} else {
result = false
break
}
}
}
})
...
})
...
})
}
复制代码
能够看到其实我只处理里 id选择器和class选择器,id和class相对来讲反作用小,引发样式异常的可能性相对较小。
判断css是否再js中出现过,是使用正则匹配。
其实,后续还能够继续优化,好比对tag类的选择器,能够配置是否再html,jsx,template中出现过,若是出现过,没有出现过也能够认为是无用代码。
固然,插件能正常工做仍是的有一些前提和约束。咱们能够在代码中动态改变css,好比再react和vue中,能够这么写
这样是比较推荐的方式,选择器做为字符或变量名出如今代码中,下面这样动态生成选择器的状况就会致使匹配失败
render(){
this.stateClass = 'state-' + this.state == 2 ? 'open' : 'close'
return <div class={this.stateClass}></div>
}
复制代码
其中这样状况很容易避免
render(){
this.stateClass = this.state == 2 ? 'state-open' : 'state-close'
return <div class={this.stateClass}></div>
}
复制代码
因此有一个好的编码规范的约束,插件能更好的工做。
若是webpack打包后的bundle文件中存在着相同的模块,也属于无用代码的一种。也应该被去除掉
首先咱们须要一个能对bundle文件定性分析的工具,能发现问题,能看出优化效果。
webpack-bundle-analyzer这个插件彻底能知足咱们的需求,他能以图形化的方式展现bundle中全部的模块的构成的各构成的大小。
其次,需求对通用模块进行提取,CommonsChunkPlugin是最被人熟知的用于提供通用模块的插件。早期的时候,我并不彻底了解他的功能,并无发挥最大的功效。
下面介绍CommonsChunkPlugin的正确用法
自动提取全部的node_moudles或者引用次数两次以上的模块
minChunks能够接受一个数值或者函数,若是是函数,可自定义打包规则
但使用上面记载的配置以后,并不能高枕无忧。由于这个配置只能提取全部entry打包后的文件中的通用模块。而现实是,有了提升性能,咱们会按需加载,经过webpack提供的import(...)方法,这种按需加载的文件并不会存在于entry之中,因此按需加载的异步模块中的通用模块并无提取。
如何提取按需加载的异步模块里的通用模块呢?
配置另外一个CommonsChunkPlugin,添加async属性,async能够接受布尔值或字符串。当时字符串时,默认是输出文件的名称。
names是全部异步模块的名称
这里还涉及一个给异步模块命名的知识点。我是这样作的:
const Edit = resolve => { import( /* webpackChunkName: "EditPage" */ './pages/Edit/Edit').then((mod) => { resolve(mod.default); }) };
const PublishPage = resolve => { import( /* webpackChunkName: "Publish" */ './pages/Publish/Publish').then((mod) => { resolve(mod); }) };
const Models = resolve => { import( /* webpackChunkName: "Models" */ './pages/Models/Models').then((mod) => { resolve(mod.default); }) };
const MediaUpload = resolve => { import( /* webpackChunkName: "MediaUpload" */ './pages/Media/MediaUpload').then((mod) => { resolve(mod); }) };
const RealTime = resolve => { import( /* webpackChunkName: "RealTime" */ './pages/RealTime/RealTime').then((mod) => { resolve(mod.default); }) };
复制代码
没错,在import里添加注释。/* webpackChunkName: "EditPage" */ ,虽然看着不舒服,可是管用。
贴一个项目的优化效果对比图
优化效果仍是比较明显。
最后思考一个问题:
不一样entry模块或按需加载的异步模块需不须要提取通用模块?
这个须要看场景了,好比模块都是在线加载的,若是通用模块提取粒度太小,会致使首页首屏须要的文件变多,不少多是首屏用不到的,致使首屏过慢,二级或三级页面加载会大幅提高。因此这个就须要根据业务场景作权衡,控制通用模块提取的粒度。
百度外卖的移动端应用场景是这样的,咱们全部的移动端页面都作了离线化的处理。离线以后,加载本地的js文件,与网络无关,基本上能够忽略文件大小,因此更关注整个离线包的大小。离线包越小,耗费用户的流量就越小,用户体验更好,因此离线化的场景是很是适合最小粒提取通用模块的,即将全部entry模块和异步加载模块的引用大于2的模块都提取,这样能得到最小的输出文件,最小的离线包。
1月20日,我将在掘金分享《百度外卖前端离线化实践》,有兴趣的能够关注一下。
文本提到的插件都是开源的,连接汇总,欢迎交流,欢迎戳❤
lin-xi/babel-plugin-import-fix