这篇文章是最近学习前端优化时的一点心得。为了对比强烈降低CPU性能,文章中的部分代码也在github上。本文中的性能截图来自chrome自带的性能。不知道的可以看看前世和他类似的chrome时间线(介绍传送门)。基础CSS选择器优化众所周知,css选择器的解析顺序是从右到左,所以#iddiv的解析速度没有div快#id减少回流重绘浏览器渲染一般流程如下:处理HTML标签并构建DOM树。处理CSS标记并构建CSS规则树。将DOM树和CSS规则树合并为一棵渲染树。根据渲染树布局计算每个节点的几何信息。将单个节点绘制到屏幕上。当由于元素的大小、布局、隐藏等变化需要重新构建RenderTree的部分或全部时,浏览器重新渲染的过程称为reflow。导致回流的操作:页面第一次呈现。浏览器窗口大小发生变化。元素大小或位置已更改。元素内容发生变化(文字数量或图片大小发生变化)。元素字体大小更改。添加或删除可见的DOM元素。激活CSS伪类。查询某些属性或调用某些方法。一些常用的引起回流的属性和方法。clientWidth,clientHeight,clientTop,clientLeftoffsetWidth,offsetHeight,offsetTop,offsetLeftscrollWidth,scrollHeight,scrollTop,scrollLeftscrollIntoView(),scrollIntoViewIfNeeded()getComputedStyle()getBoundingClientRect()scrollTo()页面中元素样式变化不影响颜色、布局背景时-color等),浏览器会赋予元素新的样式并重新绘制,这个过程称为重绘。Reflow一定会引起重绘,重绘不一定会引起reflow。缓存布局属性浏览器会维护一个队列,将所有引起回流和重绘的操作都放入队列中。如果队列中的任务数量或时间间隔达到阈值,浏览器将清空队列并进行批处理,可以将多次回流和重绘合二为一。但是,在访问上述属性和调用方法时,浏览器会清空队列以保证准确性并强制回流,所以我们可以缓存layout属性来避免这种现象。比如在滚动和加载时,我们可以缓存offsetTop等属性,避免每次比较都回流。如果你不这样做,可爱的浏览器甚至会提醒Forcedreflow是一个可能的性能瓶颈。将多次回流的元素放在absolute中并使用absolute,将会引起回流的动画带出文档流,其变化不会影响其他元素。需要注意的是,虽然float也脱离了文档流,但其他框中的文本仍然会让位给这个元素并环绕。对于使用absolute离开文档流的元素,其他框和其他框中的文本将忽略它。那才是真正的无影响。(实际测试float还不如relative。)批量修改DOM如列表。现在您需要向其中推送100个新项目。一个一个加起来,至少有100个reflow。如果用DocumentFragment处理10次,只会有10次回流。是不是只处理一次,还会回流,所以性能更好,不是。例如,我想吃100份炸鸡。如果我吃一份,我会很累。如果我一次吃100份,我会直接炒。更好的方法是分成10份,每次吃10份。.这里面涉及到的longtask概念,在接下来的优化方法中也会涉及到。任务切片的学名是task-slice,是一种必要的优化方法。让我们专注于它。首先,让我们看一下吃炸鸡的例子。为了突出优化前后的差异,将要吃的炸鸡减少到1000份。实验一:1000份一份一份吃实验二:一次吃1000份实验三:分成10次,每次吃100份,可以看到黄色条代表的脚本从一段变成了几段段落,相应的任务也从一个长条中分成几个部分。上一篇文章缓存layout属性部分提到,浏览器会维护一个队列,所以实验一和实验二的结果差别不大,因为都是放到队列中进行最后的统一处理,而task-slice做的就是把一个长任务分成几个小任务交给浏览器顺序处理,以在短时间内缓解浏览器的压力。帧数也从2增加到10+。(因为我在测试的时候阉割了性能,所以优化后的帧率还是可观的)上面的例子是一个同步任务切片,那可爱的项目经理说加10张echarts图片怎么办?其实同步和异步几乎是一样的。之前简单版的代码functionTaskSlice(num,fn){this.init(num,fn)}TaskSlice.prototype={init:(num,fn)=>{letindex=0functionnext(){if(index{chart.off()})chart.setOption(options)}因为echarts的生命周期是内部定义的一个事件,看起来比较麻烦。如果你要分片的异步任务是一个promise,就比较简单了this:初始化时传入的分片次数num和异步任务fn;然后定义了一个函数next,next通过一个闭包维护了一个函数表示当前任务执行次数的变量索引,然后调用next进入函数的逻辑;判断执行次数是否小于要切割的次数,如果小于,调用fn,给他两个参数,当前执行次数和next;然后进入fn函数,这里只需要在异步完成后调用next,任务被切割成很多块。缩小作用域寻找作用域链,类似于原型链。当我们使用一个对象的某个属性时,它会遍历原型链,从当前对象的属性开始。如果找不到该属性,它将沿着原型链向下移动。移动位置,寻找对应的属性,直到找到属性或到达原型链的末尾。在作用域链中,我们会先在当前作用域中寻找我们需要的变量或函数,如果没有找到,再寻找上一个作用域,直到找到该变量/函数或到达全局作用域。//badvara=1;functionfn(){console.log(a);}fn()//goodvara=1;functionfn(value){console.log(value);}fn(a)节流反-shakethrottle&debounce,网上的文章太多了,像lodash这样的工具库也有现成的源码。我还写了一个简化版,可能更受欢迎。在文章开头提到的github中。重要的是要注意,他们不能减少事件触发的次数。只要学习,你就完成了。懒加载首先将img标签中的src链接设置为同一张图片,并将其真实图片地址存储在img标签的自定义属性中。当js检测到图片元素进入可见窗口后,将src的值替换为自定义属性,减少首屏加载的请求次数,达到懒加载的效果。滚动事件的定义和是否进入可见窗口的计算使用了前面提到的防抖和缓存布局属性from(els)this.loadedLen=0this.init()}LazyLoad.prototype={init:function(){this.initHandler()this.lazyLoad()},load:function(el){if(!el.loaded){el.src=el.getAttribute('data-src')this.loadedLen++el.loaded=true}},lazyLoad:function(){for(leti=0;ithis.loadedLen){this.lazyLoad()}else{window.removeEventListener('scroll',this.scrollHander,false)}}},1000)this.scrollHander=fn.bind(this)window.addEventListener('scroll',this.scrollHander,false)},}Vue函数式组件可以把没有状态、没有this上下文、没有生命周期的组件写成函数式组件,因为函数式组件就是函数,所以渲染开销要低很多具体,判断网入口子组件的拆解是因为vue的渲染顺序是从父到子,所以拆解子组件类似于上面说的任务切片。就是把一个大的task分成两个task,parent和child。使用v-show重用dom以下这段话是从官网复制的v-ifis"true"条件渲染,因为它会保证条件块内的事件监听器和子组件在切换过程中被正确销毁和重建。v-show更简单——无论初始条件是什么,元素总是被渲染,并且只是基于CSS进行切换。使用keep-alive缓存keep-alive是Vue内置的一个组件,会在组件中缓存组件的实例,节省再次渲染时初始化组件的开销。DOM的延迟加载其实就是任务分片,但是这种实现方式真的很适合Vue。直接上传代码exportdefaultfunction(count=10){return{data(){return{displayPriority:0,}},mounted(){this.runDisplayPriority()},methods:{runDisplayPriority(){conststep=()=>{requestAnimationFrame(()=>{this.displayPriority++if(this.displayPriority=priority},},}}函数返回一个mixin,通过defer函数和v-if来控制切片,像这样:非响应式数据众所周知,当一个新的Vue被创建时,Vue会遍历数据中的属性来设置通过Object.defineProperty(版本2.x)将它们作为响应数据。当属性发生变化时,它将通过触发属性集来更新视图。所以如果只是定义一些常量,我们不需要vue设置为responsive,直接写在created中即可。table组件的props肯定会有一个数组。常见的写法是这样的一开始我也觉得这种写法很正常,list需要响应式,因为table需要随着list的变化而变化,何况element-ui官网上的例子就是把list的声明放在data里面。然而,真正起作用的是作为props传递到表组件中的列表,而不是父组件中的列表。所以没必要把这个列表的声明放在data中。还是以上面的table组件为例,因为vue会递归遍历data和props的所有属性,所以在传入list的时候,假设list的结构是这样的[{id:1,name:'frontend'}],那么id和name这两个属性也会被设置为响应式。如果只需要显示这两个属性,可以这样做functionoptimizeItem(item){constdata={}Object.defineProperty(data,'data',{configurable:false,value:item,})returndata}防止vue通过将属性的可配置设置为false来修改它。webpack缩小了文件搜索范围并优化了加载器配置。使用加载程序时,可以使用测试、包含和排除来命中匹配文件,以便处理尽可能少的文件。resolve.aliasresolve.alias使用别名将原始导入路径映射到新的导入路径。项目往往依赖于一些庞大的第三方模块。以反应为例。默认情况下,Webpack会使用库的package.json./node_modules/react/react.js中定义的入口文件开始递归解析和处理几十个依赖文件,这将是一个耗时的操作。通过配置resolve.alias,Webpack在处理React库时,可以直接使用一个独立完整的react.min.js文件,从而跳过耗时的递归解析操作。(vue类库的入口文件直接是一个单独完整的文件,牛逼。)一般这种方法可以用于完整性强的库,但是像loadsh这样的,可能只会用到其中的几个函数。如果也这样设置,会导致输出文件中出现大量的垃圾代码。resolve:{alias:{'react':path.resolve(__dirname,'./node_modules/react/dist/react.min.js')}},当resolve.extensions在import语句中没有文件后缀时,Webpack会按照配置的resolve.extensions后缀尝试询问文件是否存在。默认值为['.wasm','.mjs','.js','.json'](v4.41.2)。也就是说,当遇到像require('./data')这样的import语句时,Webpack会先去寻找./data.wasm这个文件,如果这个文件不存在,就会寻找./data。mjs文件等等,最后找不到就报错。如果列表更长,或者正确的后缀更晚,会造成更多的尝试,所以resolve.extensions的配置也会影响构建的性能。在配置resolve.extensions时,需要注意以下几点,尽可能优化构建性能:1.后缀尝试列表尽量少,不要把项目中不存在的条件写到后缀尝试列表。2.文件后缀出现频率高的放在前面。3、在源码中写import语句时,尽量加后缀,避免查找过程。例如,如果您确定,则将require('./data')写为require('./data.json')。resolve:{extensions:['.js','.vue'],},module.noParse该配置项允许webpack不处理不采用模块化的文件。被忽略的文件不应该有import、require等导入机制的调用。上面resolve.alias中单独完整的react.min.js没有采用模块化。忽略它可以提高构建性能。module:{noParse:[/vue\.runtime\.common\.js$/],},压缩代码浏览器从服务器访问网页时获取的JavaScript和CSS资源都是文本形式,越大文件越大,网页加载时间越长。为了提高网页加速的速度,减少网络传输流量,可以对这些资源进行压缩。js可以使用webpack内置的uglifyjs-webpack-plugin插件,css可以使用optimize-css-assets-webpack-pluginoptimization:{minimizer:[newUglifyJsPlugin(),newOptimizeCSSAssetsPlugin()]}DllPlugindll是一个动态链接库,在一个动态链接库中可以包含其他模块调用的函数和数据。包含基本第三方模块(如vue全家桶)的动态链接库只需要编译一次,后续构建时无需重新编译这些模块,直接使用动态链接库中的代码。所以会大大提高构建速度。具体操作是使用内置的两个插件DllPlugin和DllReferencePlugin。前者用于打包动态链接库文件,后者用于webpack主配置中的解引用。//Packagedllentry:{vendor:['vue','vue-router','vuex'],},output:{filename:'[name].dll.js',path:path.resolve(__dirname,'dist'),library:'_dll_[name]',},plugins:[newDllPlugin({name:'_dll_[name]',path:path.join(__dirname,'dist','[name].manifest.json'),}),],//output和plugins中的[name]都是entry中的key,//那就是'vender'//referenceplugins:[newDllReferencePlugin({manifest:require('../dist/vendor.manifest.json'),}),]happypack由于运行在Node.js上的Webpack是单线程的,所以Webpack需要处理的任务会一个一个完成,不能多个事情一起做。而HappyPack可以将任务分解为多个子进程并发执行,子进程处理完成后再将结果发送给主进程。bb上的代码不多constHappyPack=require('happypack')module:{rules:[{test:/\.js$/,use:['happypack/loader?id=babel']}],},plugins:[newHappyPack({//使用唯一标识符id来表示当前//HappyPack用于处理特定类型的文件id:'bable',loaders:['babel-loader'],})]但是HappyPack(v5.0.1)不支持vue-loader(v15.3.0)(支持列表),并且在vue项目中,如果使用模板语法,大部分业务js写在.vue文件中,可以通过配置vue-loader的options部分,js部分交给happypack。好像支持之前的vue-loader。需要在pulgins中单独声明,是不行的。vue-loader升级加快了打包速度。为了使用happypack而强行降级,有点浪费钱。//rules:[//{//test:/\.vue$/,//use:[//{//loader:'vue-loader',//options:{//loaders:{//js:'happypack/loader?id=babel'//},//}//}//]//}//]不支持也没关系,vueLoader文档说可以自己定义在pulgins.vue文件中复制并应用相应语言块的其他规则。例如,如果您有一个匹配/\.js$/的规则,它将应用于.vue文件中的