这篇文章算是最近学习前端优化时的一点心得,为了对比强烈下降了CPU性能,文中代码在 github上也有一部分。
本文性能截图来自chrome自带的performance,不了解的能够看看跟他差很少的前世chrome timeline(介绍 传送门)。
众所周知,css选择符解析顺序为从右向左,因此#id div
的解析速度就不如div #id
css
浏览器渲染大体流程是这样的:html
当 Render Tree 中部分或所有, 因元素的尺寸、布局、隐藏等改变而须要从新构建,浏览器从新渲染的过程称为回流。
会致使回流的操做:前端
DOM
元素。CSS
伪类。一些经常使用且会致使回流的属性和方法。vue
clientWidth
、clientHeight
、clientTop
、clientLeft
offsetWidth
、offsetHeight
、offsetTop
、offsetLeft
scrollWidth
、scrollHeight
、scrollTop
、scrollLeft
scrollIntoView()
、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
当页面中元素样式的改变并不影响布局时(像color
、background-color
等),浏览器会将新样式赋予给元素并从新绘制它,这个过程称为重绘。回流必将引发重绘,重绘不必定会引发回流。node
Forced reflow is a likely performance bottleneck.
float
也是脱离文档流,但其余盒子内的文本依然会为这个元素让出位置,环绕在周围。而对于使用absolute
脱离文档流的元素,其余盒子与其余盒子内的文本都会无视它。才是真正的不会影响。(实际测试float
甚至还不如relative
)DocumentFragment
分10次处理,就只会有10次回流。那是否是只处理一次,就会有一次回流,这样性能更好呢,并非。举个栗子,我想要吃100份炸鸡,若是一份一份吃会很累,若是一次直接吃100份,会直接撑炸,比较好的方式就是分10次吃,每次吃10份。这其中涉及到的long task概念,也是下一个优化方式所涉及的。学名task-slice,算是一个必备的优化方式了,着重说一哈,先来看吃炸鸡的例子,为了突出优化先后差别把要吃的炸鸡变成1000份。
实验1:一份一份吃 吃1000次
实验2:一次吃1000份
实验3:分10次,每次吃100份
能够看到黄条表明的scripting从一段变成了好几段,对应的task也从一长条分了好几份。前文中缓存layout属性的部分讲过,浏览器会维护一个队列,因此实验1和实验2结果差距不大是由于他们都被放进队列中最后统一处理,而task-slice作的就是把一个long task,分红几个小task交给浏览器依次处理,缓解浏览器短期内的压力。帧数也从2提高到了10+。(由于我测试时阉割了性能,因此优化后帧数依然感人)react
上面这个例子,是同步的任务切片,那万一可爱的项目经理说要加10个echarts图咋办嘞。
其实同步和异步差很少的,上一个简单版本的代码webpack
function TaskSlice(num, fn) { this.init(num, fn) } TaskSlice.prototype = { init: (num, fn) => { let index = 0 function next() { if(index < num) { fn.call(this, index, next) } index++ } next() } }
使用的时候就这样git
function drawCharts (num) { new TaskSlice( num, drawChart ) } function drawChart(id, cb) { var chart = echarts.init(document.getElementById(id)) chart.on('finished', cb) chart.on('finished', () => { chart.off() }) chart.setOption(options) }
由于echarts
的生命周期是本身内部定义的事件,因此看起来比较麻烦,若是想要切片的异步任务是promise
就比较简单了github
function asyncTask(cb) { promise().then(() => { // balabalaba cb() }) }
这个类的逻辑大概是这样的:
初始化时传入要切片的次数num
和异步的任务fn
;
而后定义一个函数next
,next
经过闭包维护一个表示当前执行任务次数的变量index
,而后调用next
进入函数内逻辑;
判断执行次数是否小于要切的次数,小于的话,调用fn
,同时给他两个参数分别为当前执行次数和next
;
而后进入fn
函数,这里只须要在异步完成后调用next
,任务就被切成了好多片。web
做用域链和原型链相似,当咱们使用对象的某一个属性时,会遍历原型链,从当前对象的属性开始,若是找不到该属性,则会在原型链上往下一个位置移动,寻找对应的属性,直到找到属性或者达到原型链末端。
在做用域链中,首先会在当前做用域中寻找咱们须要的变量或者函数,若是没找到的话,则往上一个做用域寻找,直到找到变量/函数或者到达全局做用域。
//bad var a=1; function fn(){ console.log(a); } fn() //good var a=1; function fn(value){ console.log(value); } fn(a)
throttle&debounce,这个网上文章太多了,并且像lodash
这种工具库也有现成的源码,我也写了一个简版的,可能更通俗一点,就在文章开头说的github里,须要注意的是他们不能减小事件的触发次数。学就完事儿了。
先将img标签中的src连接设为同一张图片,将其真正的图片地址存储在img标签的自定义属性。当js监听到该图片元素进入可视窗口时,再把src的值替换为自定义属性,减小首屏加载的请求数量,达到懒加载的效果。
其中的定义滚动事件,和计算是否进入可视窗口,就用到了前面说的防抖和缓存layout属性
let pending = false function LazyLoad({ els, lazyDistance }) { this.lazyDistance = lazyDistance this.imglist = Array.from(els) this.loadedLen = 0 this.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(let i = 0; i < this.imglist.length; i++) { this.getBound(this.imglist[i]) && this.load(this.imglist[i]) } pending = false }, getBound: function(el) { let bound = el.getBoundingClientRect() let clientHeight = document.documentElement.clientHeight || document.body.clientHeight return bound.top <= clientHeight + this.lazyDistance }, initHandler: function() { const fn = throttle(function() { if(!pending) { pending = true if(this.imglist.length > this.loadedLen) { this.lazyLoad() } else { window.removeEventListener('scroll', this.scrollHander, false) } } }, 1000) this.scrollHander = fn.bind(this) window.addEventListener('scroll', this.scrollHander, false) }, }
能够把没有状态,没有this上下文,没有生命周期的组件,写为函数式组件,由于函数式组件只是函数,因此渲染开销也低不少。具体写法官网传送门
由于vue的渲染顺序为先父到子,因此拆分子组件相似上面所说的task slice
。就是把一个大的task分红了父和子两个task。
下面这段话抄自官网
v-if 是“真正”的条件渲染,由于它会确保在切换过程当中条件块内的事件监听器和子组件适当地被销毁和重建。
v-show 就简单得多——无论初始条件是什么,元素老是会被渲染,而且只是简单地基于 CSS 进行切换。
keep-alive是Vue内置组件,会缓存该组件内的组件的实例,节省再次渲染时初始化组件的花销。
这一项其实仍是任务切片,可是这种实现方式真的和Vue特别契合,直接上代码
export default function (count = 10) { return { data () { return { displayPriority: 0, } }, mounted () { this.runDisplayPriority() }, methods: { runDisplayPriority () { const step = () => { requestAnimationFrame(() => { this.displayPriority++ if (this.displayPriority < count) { step() } }) } step() }, defer (priority) { return this.displayPriority >= priority }, }, } }
函数返回一个mixin
,经过defer
函数和v-if
来控制切片,像这样:
Object.defineProperty
(2.x版本)来将他们设置为响应式数据,当其中的属性变化时,经过触发属性的set
去更新View。那么若是只是为了定义一些常量,咱们就不须要vue去设置他们为响应式,写在created
里面就能够了。一个table组件的props确定会有一个数组,常见的写法像这样
<template> <el-table :data="list"> <!--一些内容--> </el-table> </template> <script> // 一些内容 data() { return { list: [] } } created() { this.fetch() // 获取数据赋值list } </script>
我一开始也以为这种写法无比正常,list须要是响应式的,由于须要table随着list的改变而改变,更况且element-ui
官网的示例就是将list的声明放在data中。然鹅,真正起做用的是做为props传进table组件的list,而不是再父组件中的list。因此这个list的声明也是没有必要放在data里的。
仍是以上面的table组件为例,由于vue会递归遍历data和props的全部属性,因此当list传进时,假设list的结构是这样的[{id: 1, name: '前端'}]
,那么id和name两个属性也会被设置为响应式,若是需求这两个属性只须要展现,那么能够这样作
function optimizeItem (item) { const data = {} Object.defineProperty(data, 'data', { configurable: false, value: item, }) return data }
经过设置属性的configurable为false来阻止vue再去修改他。
test
、include
、exclude
来命中匹配的文件,让尽量少的文件被处理。resolve.alias
resolve.alias 经过别名来把原导入路径映射成一个新的导入路径,在项目种常常会依赖一些庞大的第三方模块,以react为例,默认状况下 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
在导入语句没带文件后缀时,Webpack 会根据resolve.extensions
的配置带上后缀后去尝试询问文件是否存在。默认值是['.wasm', '.mjs', '.js', '.json']
(v4.41.2)。也就是说当遇到 require('./data') 这样的导入语句时,Webpack 会先去寻找 ./data.wasm 文件,若是该文件不存在就去寻找 ./data.mjs 文件,以此类推,最后若是找不到就报错。
若是这个列表越长,或者正确的后缀在越后面,就会形成尝试的次数越多,因此 resolve.extensions 的配置也会影响到构建的性能。 在配置 resolve.extensions 时你须要遵照如下几点,以作到尽量的优化构建性能:
一、后缀尝试列表要尽量的小,不要把项目中不可能存在的状况写到后缀尝试列表。
二、频率出现高的文件后缀要优先放在前面。
三、在源码中写导入语句时,要尽量的带上后缀,从而能够避免寻找过程。例如在你肯定的状况下把 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-plugin
optimization: { minimizer: [ new UglifyJsPlugin(), new OptimizeCSSAssetsPlugin() ] }
dll是动态连接库,在一个动态连接库中能够包含给其余模块调用的函数和数据。包含基础的第三方模块(如vue全家桶)的动态连接库只须要编译一次,以后的构建中这些模块就不须要从新编译,而是直接使用动态连接库中的代码。因此会大大提高构建速度。
具体操做是使用DllPlugin
和DllReferencePlugin
这两个内置的插件,前者用于打包出动态连接库文件,后者用于主webpack配置中去引用。
// 打包dll entry: { vendor: ['vue', 'vue-router', 'vuex'], }, output: { filename: '[name].dll.js', path: path.resolve(__dirname, 'dist'), library: '_dll_[name]', }, plugins: [ new DllPlugin({ name: '_dll_[name]', path: path.join(__dirname, 'dist', '[name].manifest.json'), }), ], // output和plugins中的[name]都是entry中的key, // 也就是'vender'
// 引用 plugins: [ new DllReferencePlugin({ manifest: require('../dist/vendor.manifest.json'), }), ]
因为运行在Node.js之上的Webpack是单线程的,因此Webpack须要处理的任务会一件件挨着作,不能多个事情一块儿作。而HappyPack能够把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。很少bb上代码
const HappyPack = require('happypack') module: { rules: [ { test: /\.js$/, use: ['happypack/loader?id=babel'] } ], }, plugins: [ new HappyPack({ // 用惟一的标识符 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' // }, // } // } // ] // } //]
不支持也没有关系,vue Loader文档有说,在pulgins中引用能够将你定义过的其它规则复制并应用到.vue
文件里相应语言的块。例如,若是你有一条匹配/\.js$/
的规则,那么它会应用到.vue
文件里的<script>
块。
了解这个东西是看webpack文档的时候,发现resolve.extensions
的默认配置是['.wasm', '.mjs', '.js', '.json']
,这个wasm甚至是排在第一位的,就去了解了一下,真是不看不知道一看吓一跳,这玩意儿也忒厉害咧,个人理解浏览器识别js代码的大概流程是下载->转换->编译,可是wasm能够跳过转换和编译两步,由于他自己就能够被浏览器识别,从而并且最近WebAssembly
也正式加入到W3C标准了,别问,问就是知识点。放一个[mdn对于WebAssembly的介绍]看成拓展阅读(https://developer.mozilla.org...