移动互联网时代,用户对于网页的打开速度要求愈来愈高。首屏做为直面用户的第一屏,其重要性不言而喻。优化用户体验更是咱们前端开发很是须要 focus 的东西之一。javascript
从用户的角度而言,当打开一个网页,每每关心的是从输入完网页地址后到最后展示完整页面这个过程须要的时间,这个时间越短,用户体验越好。因此做为网页的开发者,就从输入url到页面渲染呈现这个过程当中去提高网页的性能。css
因此输入URL后发生了什么呢?在浏览器中输入url会经历域名解析、创建TCP链接、发送http请求、资源解析等步骤。html
http缓存优化是网页性能优化的重要一环,这一部分我会在后续笔记中作一个详细总结,因此本文暂很少作详细整理。本文主要从网页渲染过程、网页交互以及Vue应用优化三个角度对性能优化作一个小结。前端
首先谈谈拿到服务端资源后浏览器渲染的流程:vue
1. 解析 HTML 文件,构建 DOM 树,同时浏览器主进程负责下载 CSS 文件 2. CSS 文件下载完成,解析 CSS 文件成树形的数据结构,而后结合 DOM 树合并成 RenderObject 树 3. 布局 RenderObject 树 (Layout/reflow),负责 RenderObject 树中的元素的尺寸,位置等计算 4. 绘制 RenderObject 树 (paint),绘制页面的像素信息 5. 浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面
关键渲染路径是浏览器将 HTML、CSS、JavaScript 转换为在屏幕上呈现的像素内容所经历的一系列步骤。也就是咱们刚刚提到的的的浏览器渲染流程。java
为尽快完成首次渲染,咱们须要最大限度减少如下三种可变因素:node
* 关键资源的数量: 可能阻止网页首次渲染的资源。 * 关键路径长度: 获取全部关键资源所需的往返次数或总时间。 * 关键字节: 实现网页首次渲染所需的总字节数,等同于全部关键资源传送文件大小的总和。
* 删除没必要要的代码和注释包括空格,尽可能作到最小化文件。 * 能够利用 GZIP 压缩文件。 * 结合 HTTP 缓存文件。
首先,DOM 和 CSSOM 一般是并行构建的,因此 CSS 加载不会阻塞 DOM 的解析。webpack
然而,因为 Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,
因此他必须等待到 CSSOM Tree 构建完成,也就是 CSS 资源加载完成(或者 CSS 资源加载失败)后,才能开始渲染。所以,CSS 加载会阻塞 Dom 的渲染。git
因而可知,对于 CSSOM 缩小、压缩以及缓存一样重要,咱们能够从这方面考虑去优化。github
* 减小关键 CSS 元素数量 * 当咱们声明样式表时,请密切关注媒体查询的类型,它们极大地影响了 CRP 的性能 。
当浏览器遇到 script 标记时,会阻止解析器继续操做,直到 CSSOM 构建完毕,JavaScript 才会运行并继续完成 DOM 构建过程。
* async: 当咱们在 script 标记添加 async 属性之后,浏览器遇到这个 script 标记时会继续解析 DOM,同时脚本也不会被 CSSOM 阻止,即不会阻止 CRP。 * defer: 与 async 的区别在于,脚本须要等到文档解析后( DOMContentLoaded 事件前)执行,而 async 容许脚本在文档解析时位于后台运行(二者下载的过程不会阻塞 DOM,但执行会)。 * 当咱们的脚本不会修改 DOM 或 CSSOM 时,推荐使用 async 。 * 预加载 —— preload & prefetch 。 * DNS 预解析 —— dns-prefetch 。
* 分析并用 **关键资源数 关键字节数 关键路径长度** 来描述咱们的 CRP 。 * 最小化关键资源数: 消除它们(内联)、推迟它们的下载(defer)或者使它们异步解析(async)等等 。 * 优化关键字节数(缩小、压缩)来减小下载时间 。 * 优化加载剩余关键资源的顺序: 让关键资源(CSS)尽早下载以减小 CRP 长度 。
补充阅读: 前端性能优化之关键路径渲染优化
回流必将引发重绘,重绘不必定会引发回流。
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility 等),浏览器会将新样式赋予给元素并从新绘制它,这个过程称为重绘。
当 Render Tree 中部分或所有元素的尺寸、结构、或某些属性发生改变时,浏览器从新渲染部分或所有文档的过程称为回流。
会致使回流的操做:
* 页面首次渲染 * 浏览器窗口大小发生改变 * 元素尺寸或位置发生改变元素内容变化(文字数量或图片大小等等) * 元素字体大小变化 * 添加或者删除可见的 DOM 元素 * 激活 CSS 伪类(例如:hover) * 查询某些属性或调用某些方法 * 一些经常使用且会致使回流的属性和方法
clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftscrollIntoView()、scrollIntoViewIfNeeded()、getComputedStyle()、 getBoundingClientRect()、scrollTo()
回流比重绘的代价要更高。
有时即便仅仅回流一个单一的元素,它的父元素以及任何跟随它的元素也会产生回流。现代浏览器会对频繁的回流或重绘操做进行优化:浏览器会维护一个队列,把全部引发回流和重绘的操做放入队列中,若是队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样能够把屡次回流和重绘变成一次。
当你访问如下属性或方法时,浏览器会马上清空队列:
clientWidth、clientHeight、clientTop、clientLeft offsetWidth、offsetHeight、offsetTop、offsetLeft scrollWidth、scrollHeight、scrollTop、scrollLeft width、height getComputedStyle() getBoundingClientRect()
由于队列中可能会有影响到这些属性或方法返回值的操做,即便你但愿获取的信息与队列中操做引起的改变无关,浏览器也会强行清空队列,确保你拿到的值是最精确的。
CSS
Javascript
// 优化前 const el = document.getElementById('test'); el.style.borderLeft = '1px'; el.style.borderRight = '2px'; el.style.padding = '5px'; // 优化后,一次性修改样式,这样能够将三次重排减小到一次重排 const el = document.getElementById('test'); el.style.cssText += '; border-left: 1px ;border-right: 2px; padding: 5px;'
图片懒加载在一些图片密集型的网站中运用比较多,经过图片懒加载可让一些不可视的图片不去加载,避免一次性加载过多的图片致使请求阻塞(浏览器通常对同一域名下的并发请求的链接数有限制),这样就能够提升网站的加载速度,提升用户体验。
将页面中的img标签src指向一张小图片或者src为空,而后定义data-src(这个属性能够自定义命名,我才用data-src)属性指向真实的图片。src指向一张默认的图片,不然当src为空时也会向服务器发送一次请求。能够指向loading的地址。注意,图片要指定宽高。
<img src="default.jpg" data-src="666.jpg" />
当载入页面时,先把可视区域内的img标签的data-src属性值负给src,而后监听滚动事件,把用户即将看到的图片加载。这样便实现了懒加载。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <style> img { display: block; margin-bottom: 50px; width: 400px; height: 400px; } </style> </head> <body> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <img src="Go.png" data-src="./lifecycle.jpeg" alt=""> <script> let num = document.getElementsByTagName('img').length; let img = document.getElementsByTagName("img"); let n = 0; //存储图片加载到的位置,避免每次都从第一张图片开始遍历 lazyload(); //页面载入完毕加载但是区域内的图片 window.onscroll = lazyload; function lazyload() { //监听页面滚动事件 let seeHeight = document.documentElement.clientHeight; //可见区域高度 let scrollTop = document.documentElement.scrollTop || document.body.scrollTop; //滚动条距离顶部高度 for (let i = n; i < num; i++) { if (img[i].offsetTop < seeHeight + scrollTop) { if (img[i].getAttribute("src") == "Go.png") { img[i].src = img[i].getAttribute("data-src"); } n = i + 1; } } } </script> </body> </html>
事件委托其实就是利用JS事件冒泡机制把本来须要绑定在子元素的响应事件(click、keydown……)委托给父元素,让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。
优势:
1. 大量减小内存占用,减小事件注册。 2. 新增元素实现动态绑定事件
例若有一个列表须要绑定点击事件,每个列表项的点击都须要返回不一样的结果。
传统写法:
<ul id="color-list"> <li>red</li> <li>yellow</li> <li>blue</li> <li>green</li> <li>black</li> <li>white</li> </ul> <script> (function () { var color_list = document.querySelectorAll('li') console.log("color_list", color_list) for (let item of color_list) { item.onclick = showColor; } function showColor(e) { alert(e.target.innerHTML) console.log("showColor -> e.target", e.target.innerHTML) } })(); </script>
传统方法会利用for循环遍历列表为每个列表元素绑定点击事件,当列表中元素数量很是庞大时,须要绑定大量的点击事件,这种方式就会产生性能问题。这种状况下利用事件委托就能很好的解决这个问题。
改用事件委托:
<ul id="color-list"> <li>red</li> <li>yellow</li> <li>blue</li> <li>green</li> <li>black</li> <li>white</li> </ul> <script> (function () { var color_list = document.getElementByid('color-list'); color_list.addEventListener('click', showColor, true); function showColor(e) { var x = e.target; if (x.nodeName.toLowerCase() === 'li') { alert(x.innerHTML); } } })(); </script>
输入搜索时,能够用防抖debounce等优化方式,减小http请求;
这里以滚动条事件举例:防抖函数 onscroll 结束时触发一次,延迟执行
function debounce(func, wait) { let timeout; return function() { let context = this; // 指向全局 let args = arguments; if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { func.apply(context, args); // context.func(args) }, wait); }; } // 使用 window.onscroll = debounce(function() { console.log('debounce'); }, 1000);
节流函数:只容许一个函数在N秒内执行一次。滚动条调用接口时,能够用节流throttle等优化方式,减小http请求;
下面仍是一个简单的滚动条事件节流函数:节流函数 onscroll 时,每隔一段时间触发一次,像水滴同样
function throttle(fn, delay) { let prevTime = Date.now(); return function() { let curTime = Date.now(); if (curTime - prevTime > delay) { fn.apply(this, arguments); prevTime = curTime; } }; } // 使用 var throtteScroll = throttle(function() { console.log('throtte'); }, 1000); window.onscroll = throtteScroll;
Vue 应用的性能问题能够分为两个部分,第一部分是运行时性能问题,第二部分是加载性能问题。
和其余 web 应用同样,定位 Vue 应用性能问题最好的工具是 Chrome Devtool,经过 Performance 工具能够用来录制一段时间的 CPU 占用、内存占用、FPS 等运行时性能问题,经过 Network 工具能够用来分析加载性能问题。
更多 Chrome Devtool 使用方式请参考 使用 Chrome Devtool 定位性能问题 的指南
运行时性能主要关注 Vue 应用初始化以后对 CPU、内存、本地存储等资源的占用,以及对用户交互的及时响应。
开发环境下,Vue 会提供不少警告来帮你对付常见的错误与陷阱。而在生产环境下,这些警告语句没有用,反而会增长应用的体积。有些警告检查还有一些小的运行时开销。
当使用 webpack 或 Browserify 相似的构建工具时,Vue 源码会根据 process.env.NODE_ENV 决定是否启用生产环境模式,默认状况为开发环境模式。在 webpack 与 Browserify 中都有方法来覆盖此变量,以启用 Vue 的生产环境模式,同时在构建过程当中警告语句也会被压缩工具去除。
详细的作法请参阅 生产环境部署
当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。一般状况下这个过程已经足够快了,但对性能敏感的应用仍是最好避免这种用法。
预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,因此构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。
详细的作法请参阅 预编译模板
当使用单文件组件时,组件内的 CSS 会以 <style> 标签的方式经过 JavaScript 动态注入。这有一些小小的运行时开销,将全部组件的 CSS 提取到同一个文件能够避免这个问题,也会让 CSS 更好地进行压缩和缓存。
查阅这个构建工具各自的文档来了解更多:
Object.freeze() 能够冻结一个对象,冻结以后不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。
当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象全部的属性,并使用 Object.defineProperty 把这些属性所有转为 getter/setter,这些 getter/setter 对用户来讲是不可见的,可是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。
但 Vue 在遇到像 Object.freeze() 这样被设置为不可配置以后的对象属性时,不会为对象加上 setter getter 等数据劫持的方法。 参考 Vue 源码
Object.freeze()### 应用场景
因为 Object.freeze() 会把对象冻结,因此比较适合展现类的场景,若是你的数据属性须要改变,能够从新替换成一个新的 Object.freeze()的对象。
不少时候,咱们会发现接口返回的信息是以下的深层嵌套的树形结构:
{ "id": "123", "author": { "id": "1", "name": "Paul" }, "title": "My awesome blog post", "comments": [ { "id": "324", "commenter": { "id": "2", "name": "Nicole" } } ] }
假如直接把这样的结构存储在 store 中,若是想修改某个 commenter 的信息,咱们须要一层层去遍历找到这个用户的信息,同时有可能这个用户的信息出现了屡次,还须要把其余地方的用户信息也进行修改,每次遍历的过程会带来额外的性能开销。
假设咱们把用户信息在 store 内统一存放成 users[id]这样的结构,修改和读取用户信息的成本就变得很是低。
你能够手动去把接口里的信息经过相似数据的表同样像这样存起来,也能够借助一些工具,这里就须要提到一个概念叫作 JSON数据规范化(normalize), Normalizr 是一个开源的工具,能够将上面的深层嵌套的 JSON 对象经过定义好的 schema 转变成使用 id 做为字典的实体表示的对象。
当你有让 Vue App 离线可用,或者有接口出错时候进行灾备的需求的时候,你可能会选择把 Store 数据进行持久化,这个时候须要注意如下几个方面:
Vue 社区中比较流行的 vuex-persistedstate,利用了 store 的 subscribe 机制,来订阅 Store 数据的 mutation,若是发生了变化,就会写入 storage 中,默认用的是 localstorage 做为持久化存储。
也就是说默认状况下每次 commit 都会向 localstorage 写入数据,localstorage 写入是同步的,并且存在不小的性能开销,若是你想打造 60fps 的应用,就必须避免频繁写入持久化数据。
咱们应该尽可能减小直接写入 Storage 的频率:
* 屡次写入操做合并为一次,好比采用函数节流或者将数据先缓存在内存中,最后在一并写入 * 只有在必要的时候才写入,好比只有关心的模块的数据发生变化的时候才写入
因为持久化缓存的容量有限,好比 localstorage 的缓存在某些浏览器只有 5M,咱们不能无限制的将全部数据都存起来,这样很容易达到容量限制,同时数据过大时,读取和写入操做会增长一些性能开销,同时内存也会上涨。
尤为是将 API 数据进行 normalize 数据扁平化后以后,会将一份数据散落在不一样的实体上,下次请求到新的数据也会散落在其余不一样的实体上,这样会带来持续的存储增加。
所以,当设计了一套持久化的数据缓存策略的时候,同时应该设计旧数据的缓存清除策略,例如请求到新数据的时候将旧的实体逐个进行清除。
若是你的应用存在很是长或者无限滚动的列表,那么采用 窗口化 的技术来优化性能,只须要渲染少部分区域的内容,减小从新渲染组件和建立 dom 节点的时间。
vue-virtual-scroll-list 和 vue-virtual-scroller 都是解决这类问题的开源项目。你也能够参考 Google 工程师的文章 Complexities of an Infinite Scroller 来尝试本身实现一个虚拟的滚动列表来优化性能,主要使用到的技术是 DOM 回收、墓碑元素和滚动锚定。
Google 工程师绘制的无限列表设计
上面提到的无限列表的场景,比较适合列表内元素很是类似的状况,不过有时候,你的 Vue 应用的超长列表内的内容每每不尽相同,例如在一个复杂的应用的主界面中,整个主界面由很是多不一样的模块组成,而用户看到的每每只有首屏一两个模块。在初始渲染的时候不可见区域的模块也会执行和渲染,带来一些额外的性能开销。
使用组件懒加载在不可见时只须要渲染一个骨架屏,不须要真正渲染组件
你能够对组件直接进行懒加载,对于不可见区域的组件内容,直接不进行加载和初始化,避免初始化渲染运行时的开销。具体能够参考咱们以前的专栏文章 性能优化之组件懒加载: Vue Lazy Component 介绍 ,了解如何作到组件粒度的懒加载。
在一个单页应用中,每每只有一个 html 文件,而后根据访问的 url 来匹配对应的路由脚本,动态地渲染页面内容。单页应用比较大的问题是首屏可见时间过长。
单页面应用显示一个页面会发送屡次请求,第一次拿到 html 资源,而后经过请求再去拿数据,再将数据渲染到页面上。并且因为如今微服务架构的存在,还有可能发出屡次数据请求才能将网页渲染出来,每次数据请求都会产生 RTT(往返时延),会致使加载页面的时间拖的很长。
服务端渲染、预渲染和客户端渲染的对比
这种状况下能够采用服务端渲染(SSR)和预渲染(Prerender)来提高加载性能,这两种方案,用户读取到的直接就是网页内容,因为少了节省了不少 RTT(往返时延),同时,还能够对一些资源内联在页面,能够进一步提高加载的性能。
能够参考专栏文章 优化向:单页应用多路由预渲染指南 了解如何利用预渲染进行优化。
服务端渲染(SSR)能够考虑使用 Nuxt 或者按照 Vue 官方提供的 Vue SSR 指南 来一步步搭建。
在上面提到的超长应用内容的场景中,经过组件懒加载方案能够优化初始渲染的运行性能,其实,这对于优化应用的加载性能也颇有帮助。
组件粒度的懒加载结合异步组件和 webpack 代码分片,能够保证按需加载组件,以及组件依赖的资源、接口请求等,比起一般单纯的对图片进行懒加载,更进一步的作到了按需加载资源。
使用组件懒加载以前的请求瀑布图
使用组件懒加载以后的请求瀑布图
使用组件懒加载方案对于超长内容的应用初始化渲染颇有帮助,能够减小大量必要的资源请求,缩短渲染关键路径,具体作法请参考咱们以前的专栏文章 性能优化之组件懒加载: Vue Lazy Component 介绍 。
上面部分总结了 Vue 应用运行时以及加载时的一些性能优化措施,下面作一个回顾和归纳:
Vue 应用运行时性能优化措施
Vue 应用加载性能优化措施
文章总结的这些性能优化手段固然不能覆盖全部的 Vue 应用性能问题,咱们也会不断总结和补充其余问题及优化措施,但愿文章中提到这些实践经验能给你的 Vue 应用性能优化工做带来小小的帮助。
参考:
从 8 道面试题看浏览器渲染过程与性能优化
Vue 应用性能优化指南
前端性能优化的经常使用手段
前端性能优化之关键路径渲染优化
网页页面性能优化总结
推荐阅读:
【专题:JavaScript进阶之路】
JavaScript中各类源码实现(前端面试笔试必备)
深刻理解 ES6 Promise
JavaScript之函数柯理化
ES6 尾调用和尾递归
Git经常使用命令小结
浅谈 MVC 和 MVVM 模型
我是Cloudy,现居上海,年轻的前端攻城狮一枚,爱专研,爱技术,爱分享。
我的笔记,整理不易,感谢关注
、阅读
、点赞
和收藏
。
文章有任何问题欢迎你们指出,也欢迎你们一块儿交流各类前端问题!