前端性能调优

前言

移动互联网时代,用户对于网页的打开速度要求越来越高。首屏作为直面用户的第一屏,其重要性不言而喻。优化用户体验更是我们前端开发非常需要 focus 的东西之一。

从用户的角度而言,当打开一个网页,往往关心的是从输入完网页地址后到最后展现完整页面这个过程需要的时间,这个时间越短,用户体验越好。所以作为网页的开发者,就从输入url到页面渲染呈现这个过程中去提升网页的性能。

所以输入URL后发生了什么呢?在浏览器中输入url会经历域名解析、建立TCP连接、发送http请求、资源解析等步骤。

http缓存优化是网页性能优化的重要一环,这一部分我会在后续笔记中做一个详细总结,所以本文暂不多做详细整理。本文主要从网页渲染过程、网页交互以及Vue应用优化三个角度对性能优化做一个小结。

一、页面加载及渲染过程优化

浏览器渲染流程

首先谈谈拿到服务端资源后浏览器渲染的流程:

image
1\. 解析 HTML 文件,构建 DOM 树,同时浏览器主进程负责下载 CSS 文件
2\. CSS 文件下载完成,解析 CSS 文件成树形的数据结构,然后结合 DOM 树合并成 RenderObject 树
3\. 布局 RenderObject 树 (Layout/reflow),负责 RenderObject 树中的元素的尺寸,位置等计算
4\. 绘制 RenderObject 树 (paint),绘制页面的像素信息
5\. 浏览器主进程将默认的图层和复合图层交给 GPU 进程,GPU 进程再将各个图层合成(composite),最后显示出页面

CRP(关键渲染路径Critical Rendering Path)优化

关键渲染路径是浏览器将 HTML、CSS、JavaScript 转换为在屏幕上呈现的像素内容所经历的一系列步骤。也就是我们刚刚提到的的的浏览器渲染流程。

为尽快完成首次渲染,我们需要最大限度减小以下三种可变因素:

* 关键资源的数量: 可能阻止网页首次渲染的资源。
* 关键路径长度: 获取所有关键资源所需的往返次数或总时间。
* 关键字节: 实现网页首次渲染所需的总字节数,等同于所有关键资源传送文件大小的总和。

优化 DOM

* 删除不必要的代码和注释包括空格,尽量做到最小化文件。
* 可以利用 GZIP 压缩文件。
* 结合 HTTP 缓存文件。

优化 CSSOM

首先,DOM 和 CSSOM 通常是并行构建的,所以 CSS 加载不会阻塞 DOM 的解析

然而,由于 Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,
所以他必须等待到 CSSOM Tree 构建完成,也就是 CSS 资源加载完成(或者 CSS 资源加载失败)后,才能开始渲染。因此,CSS 加载会阻塞 Dom 的渲染

由此可见,对于 CSSOM 缩小、压缩以及缓存同样重要,我们可以从这方面考虑去优化。

* 减少关键 CSS 元素数量
* 当我们声明样式表时,请密切关注媒体查询的类型,它们极大地影响了 CRP 的性能 。

优化 JavaScript

当浏览器遇到 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 长度 。

补充阅读: 前端性能优化之关键路径渲染优化

浏览器重绘(Repaint)和回流(Reflow)

回流必将引起重绘,重绘不一定会引起回流。

重绘(Repaint)

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility 等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

回流(Reflow)

当 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

  • 避免使用 table 布局。
  • 尽可能在 DOM 树的最末端改变 class。
  • 避免设置多层内联样式。
  • 将动画效果应用到 position 属性为 absolute 或 fixed 的元素上。
  • 避免使用 CSS 表达式(例如:calc())。

Javascript

  • 避免频繁操作样式,最好一次性重写 style 属性,或者将样式列表定义为 class 并一次性更改 class 属性。
// 优化前
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;'

  • 避免频繁操作 DOM,创建一个 documentFragment,在它上面应用所有 DOM 操作,最后再把它添加到文档中。
  • 也可以先为元素设置 display: none,操作结束后再把它显示出来。因为在 display 属性为 none 的元素上进行的 DOM 操作不会引发回流和重绘。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

图片懒加载

图片懒加载在一些图片密集型的网站中运用比较多,通过图片懒加载可以让一些不可视的图片不去加载,避免一次性加载过多的图片导致请求阻塞(浏览器一般对同一域名下的并发请求的连接数有限制),这样就可以提高网站的加载速度,提高用户体验。

原理

将页面中的img标签src指向一张小图片或者src为空,然后定义data-src(这个属性可以自定义命名,我才用data-src)属性指向真实的图片。src指向一张默认的图片,否则当src为空时也会向服务器发送一次请求。可以指向loading的地址。注意,图片要指定宽高。



当载入页面时,先把可视区域内的img标签的data-src属性值负给src,然后监听滚动事件,把用户即将看到的图片加载。这样便实现了懒加载。

实例




  
  


  
  
  
  
  
  
  
  
  
  
  
  




事件委托

事件委托其实就是利用JS事件冒泡机制把原本需要绑定在子元素的响应事件(click、keydown……)委托给父元素,让父元素担当事件监听的职务。事件代理的原理是DOM元素的事件冒泡。
优点

1\. 大量减少内存占用,减少事件注册。
2\. 新增元素实现动态绑定事件

例如有一个列表需要绑定点击事件,每一个列表项的点击都需要返回不同的结果。

传统写法:

  • red
  • yellow
  • blue
  • green
  • black
  • white

传统方法会利用for循环遍历列表为每一个列表元素绑定点击事件,当列表中元素数量非常庞大时,需要绑定大量的点击事件,这种方式就会产生性能问题。这种情况下利用事件委托就能很好的解决这个问题。

改用事件委托:

  • red
  • yellow
  • blue
  • green
  • black
  • white

二、渲染完成后的页面交互优化:

防抖(debounce)/节流(throttle)

防抖(debounce)

输入搜索时,可以用防抖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);

节流(throttle)

节流函数:只允许一个函数在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相关性能优化

如何定位 Vue 应用性能问题

Vue 应用的性能问题可以分为两个部分,第一部分是运行时性能问题,第二部分是加载性能问题。

和其他 web 应用一样,定位 Vue 应用性能问题最好的工具是 Chrome Devtool,通过 Performance 工具可以用来录制一段时间的 CPU 占用、内存占用、FPS 等运行时性能问题,通过 Network 工具可以用来分析加载性能问题。

image

更多 Chrome Devtool 使用方式请参考 使用 Chrome Devtool 定位性能问题 的指南

Vue 应用运行时性能优化建议

运行时性能主要关注 Vue 应用初始化之后对 CPU、内存、本地存储等资源的占用,以及对用户交互的及时响应。

引入生产环境的 Vue 文件

开发环境下,Vue 会提供很多警告来帮你对付常见的错误与陷阱。而在生产环境下,这些警告语句没有用,反而会增加应用的体积。有些警告检查还有一些小的运行时开销

当使用 webpack 或 Browserify 类似的构建工具时,Vue 源码会根据 process.env.NODE_ENV 决定是否启用生产环境模式,默认情况为开发环境模式。在 webpack 与 Browserify 中都有方法来覆盖此变量,以启用 Vue 的生产环境模式,同时在构建过程中警告语句也会被压缩工具去除。
详细的做法请参阅 生产环境部署

使用单文件组件预编译模板

当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法

预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。

详细的做法请参阅 预编译模板

提取组件的 CSS 到单独到文件

当使用单文件组件时,组件内的 CSS 会以