让你的网页更丝滑（全）
# 让你的网页更丝滑（全）

> 这篇文章是2019年5月11号，我在上海FDConf2019上的分享整理。
> 
> * 演讲主题：【让你的网页更丝滑】
> * 时间：2019年5月11日（下午）
> * 地点：上海 - FDCon2019 - B会场（全栈&全端专场）
> * 演讲嘉宾：刘博文
> 
> PPT地址：[https://ppt.baomitu.com/d/b267a4a3](https://ppt.baomitu.com/d/b267a4a3)

![让你的网页更丝滑](https://p5.ssl.qhimg.com/t01aae8e627c5c0f190.png)

## 简介

大家好，我叫刘博文，今天给大家分享的主题叫《让你的网页更丝滑》，其实就是更流畅的意思。

简单介绍一下自己，2012年我从中专毕业，当时是17岁，2015年我加入了360最大的前端团队奇舞团，那一年我是20岁；2017年由于组织架构的变动，我们组被拆分到360导航，所以我就变成360导航的一名前端工程师；2018年就是去年，因为公司是W3C的会员，所以我就加入了W3C的性能工作组。

![自我介绍](https://p0.ssl.qhimg.com/t01a0d4d64739d0498c.png)

消息比较灵通的应该听说过我在上个月出版了一本讲Vue的书，叫做[《深入浅出Vue.js》](https://item.jd.com/12573168.html)。

![图片](https://uploader.shimo.im/f/hYBrbZeZstghdqlF.png!thumbnail)

虽然出版了一本Vue的书，但其实从去年加入W3C性能工作组之后，我一直在学习和了解Web性能领域相关的知识。

## 什么样的网页是流畅的

在讨论如何让网页更流畅之前，需要先思考一个问题就是什么样的网页是流畅的？

这个问题我总结了一句话：**在网页与用户产生交互的过程中，让用户感觉流畅**。

![图片](https://p4.ssl.qhimg.com/t01cf0cab8cc55f9399.png)

你的网页不一定要有多快，它没有一个标准，你的标准就是让用户感觉流畅就够了。另一个重点就是说在**交互**过程中，让用户感到流畅。所以延伸出一个问题，如何通过**交互**让用户感觉流畅。这里面我把交互总结为两种类型，一种是被动的，一种是主动的。

![图片](https://uploader.shimo.im/f/w55hH2fipLI7n1oZ.png!thumbnail)

所谓被动交互就是不需要用户主动去触发什么，就可以让网页在视觉上与用户产生交互。
比如说：Animation（动画）、开屏广告、自动播放的轮播图等都算被动交互。与之相反，需要用户主动去触发某些行为从而产生的反馈，我称它为主动交互，比如说用鼠标点某一个按纽产生的反馈，或使用键盘按下了某个键位产生的反馈。这个反馈可以是动画，任何东西都可以。那么被动交互如何让用户感觉流畅？这是今天第一个关于优化的话题。

### 被动交互如何让用户感觉流畅

我在京东上搜索显示器，发现有一个筛选条件叫刷新率，最低的是60HZ，高的可以达到165HZ以上。

这个60HZ是什么意思？就是指屏幕每秒钟刷新60次。所以我们可以通过屏幕作为参考，如果我们的网页也可以每秒钟往屏幕传输60个画面，用户就会觉得这个网页是流畅的，有一个单位叫做FPS，意思就是每秒钟往屏幕上传输的图像数量。FPS达到60，用户就会觉得这个网页比较流程，换算下来，每一帧是16.7毫秒。

![图片](https://uploader.shimo.im/f/xpgi4CTFtMAL4zAW.png!thumbnail)

### 主动交互如何让用户感觉流畅

主动交互如何让用户感觉流畅？我也把它总结成一句话，这句话叫：“通过响应的时间影响用户的感觉”。就是说我们可以通过操控这个时间来影响用户对网页的感觉。

![图片](https://uploader.shimo.im/f/tiHeWQH1cIIZggs4.png!thumbnail)

我们看一个演示（Demo），这个演示很简单，就是我点击按纽的时候，我让这个函数延迟多少秒，然后把这个方块改变一下颜色。这下面是八个按纽，分别是10毫秒、30毫秒、50毫秒、100毫秒、200毫秒、300毫秒、500毫秒、1秒。（文章无法演示，可以到在线PPT里去体验，或者访问[https://code.h5jun.com/pojob](https://code.h5jun.com/pojob)）

![图片](https://uploader.shimo.im/f/N2Xw2LQJOKAyuPTm.png!thumbnail)

你会发现当我点击200毫秒的按钮时，这个反馈速度，用户会觉得这个东西有一点卡，当我点击100毫秒的按钮时，已经感觉不卡了，当然更快更好。所以你会发现**100毫秒是一个临界点**，从我们的输入，包括键盘按键和鼠标点击到最终输出到眼睛里，这个时间100毫秒是临界点。超过这个时间，用户就会觉得有点卡，所以100毫秒是关键点。

![图片](https://p0.ssl.qhimg.com/t016d2988a7f37a5095.png)

我们再看一个例子，代码和刚才是一样的，现在只有一个按纽是100毫秒，刚才我说100毫秒，用户就会觉得很流畅。其实你会发现还是卡一下，但是不是说每次都卡，有的时候不卡，为什么有的时候卡有的时候不卡？

因为我们的目标是从输入到输出总时间是100毫秒以内，用户才会觉得流畅。但其实我这个代码有一个问题是这个函数的执行时间是100毫秒，所以如果当我点击这个按纽一瞬间，如果有其他任务在执行，就会把我这个函数堵塞住，被阻塞的时间加上函数执行的100毫秒，现在整体时间已经超过100毫秒，所以我刚才点击这个按纽，你会发现有时候卡，有时候不卡，不卡的时候是因为我点击这个按纽的时候，恰巧没有其他的任务在执行。

所以为什么会有这个问题？因为大家都知道JS是单线程的，浏览器同一时间内只能执行一个任务，所以为了避免这个问题，解决方案就是说所有的任务执行时间不能超过50毫秒。如果我所有的任务都不超过50毫秒，假设最糟糕的情况下，我点击这个按纽的一瞬间，有其他的任务在执行，但其实他的任务执行时间最多是50毫秒，我的任务执行时间也是保持在50毫秒以内，其实总共也不会超过100毫秒，所以用户依然会觉得很流畅，即便是最糟糕的情况下。

![图片](https://p5.ssl.qhimg.com/t01abe8fd1f6ed76cc0.png)

可以看一下这个粉色的地方，从input到response总时间是100毫秒，红色区域是被阻塞的部分，黄色是函数执行的时间和时机，你会发现我这两个任务都保持在50毫秒以内的情况下，我可以保证我的总时间是100毫秒以内完成的，这个50毫秒不是我定的，W3C性能工作组有一个Longtask规范也对这种情况做了规定。

![图片](https://uploader.shimo.im/f/keMNEvkmJ2gRC0tY.png!thumbnail)

这个规范就规定所有的任务，包括函数执行，包括什么都算上，不能超过50毫秒，超过50毫秒就被定义为长任务，所谓长任务就是执行时间过长的任务，这是不合理的，应该被解决的任务。性能监控一般都会通过图中的代码来监控与捕获长任务，可以看到这个entryType是longtask的。

![图片](https://uploader.shimo.im/f/Flvmmtk50t8sbKaq.png!thumbnail)

总结一下，如何让用户感觉流畅？就是响应时间保持在100毫秒以内，动画要16.7毫秒传输一帧到屏幕上，空闲任务不能超过50毫秒，其实不只是空闲任务，所有任务都不能超过50毫秒，加载时间是1000毫秒，所谓的页面秒开就是从这里来的。这四个单词的首字母加在一起组成一个单词叫RAIL，这是一个术语，它代表以用户为中心的性能模型，我们刚才讲的也是这个话题，感兴趣大家可以回去查一下。

## 像素管道

今天讲第二个概念叫像素管道。所谓像素管道，就是说我们通常会在网页触发一些视觉变化，你用JS改了颜色和宽度等等，随后浏览器就会做样式计算，浏览器还会做布局、绘制，合并图层等，这个过程叫做像素管道。

![图片](https://p4.ssl.qhimg.com/t01cbdd050de1753d80.png)

但是有的时候，不是所有的样式都会触发布局，有的时候不需要布局的，我们通过一些优化手段也可以取消Paint（绘制）这一步。有一个网站叫 csstriggers，可以看哪些属性触发了布局，哪些触发了Paint，这个网站有列表可以看。

## 避免长任务

今天第一个关于如何优化的话题叫**如何保证主动交互让用户感觉流畅**，其实刚才我们介绍说想保证主动交互让用户感觉流畅需要**避免长任务**，所以这个副标题叫**如何避免长任务**。

![图片](https://uploader.shimo.im/f/me5IRa7HFpYIqUjp.png!thumbnail)

如何避免长任务，有两种方案：一种叫 Web Worker ，还有一种方案叫 Time Slicing（时间切片）。

![图片](https://uploader.shimo.im/f/TrRPdBARbHwtP2sh.png!thumbnail)

### Web Worker 

先说Web Worker，我们看一段代码，我的网页里面有一个while循环，通常来讲这个循环会把浏览器卡死一秒钟，因为循环了一秒，现在我把它移动到 worker中 执行，就不会卡死浏览器了，它在worker线层中工作，就不会卡死主线程。这是一种解决方案，可以看一下效果。（由于文章无法演示效果，感兴趣的小伙伴可以到在线PPT里观察 [https://ppt.baomitu.com/d/b267a4a3#/14](https://ppt.baomitu.com/d/b267a4a3#/14)）

```javascript
const testWorker = new Worker('./worker.js')
setTimeout(_ => {
  testWorker.postMessage({})
  testWorker.onmessage = function (ev) {
    console.log(ev.data)
  }
}, 5000)

// worker.js
self.onmessage = function () {
  const start = performance.now()
  while (performance.now() - start < 1000) {}
  postMessage('done!')
}
```

可以看到现在浏览器没有被堵塞掉。

![图片](https://p1.ssl.qhimg.com/t01aa5887cfa552427e.png)

我们通过捕获火焰图，发现优化前其实长任务是主线程中工作，优化之后是放在 Worker 来进行的，所以我的主线依然可以处理其他的任务。

Web Worker虽然好，但是它有一个缺陷，就是它没有办法摸DOM。如果你想操作DOM，那么就没法在Worker中执行。我就是要循环超过100毫秒，我又想在循环中操作DOM，这时候怎么办？有一个方案叫 Time Slicing。

### Time Slicing

Time Slicing就是把一个长任务给切割成无数个执行时间很短的任务。

![图片](https://uploader.shimo.im/f/iplVgQ59zKA15Ji9.png!thumbnail)

可以看到中间用户红框框起来的，内部有很多黄颜色的小竖线，其实每一个都是任务，放大之后，就是图中最下面的火焰图，可以看到中间是有空隙的。因为中间有空隙，浏览器就可以在这些空隙中做其他的事，比方说布局、样式计算、UI事件，所有事情都可以做。

实现时间切片功能的代码也并不是很复杂，就是下面这段代码，其实核心代码只有三四行。代码虽然不多，但是可能理解起来也没有那么容易，我为大家简单介绍一下。

```javascript
function block () {
  ts(function* () {
    const start = performance.now()
    while (performance.now() - start < 1000) {
      console.log(11)
      yield
    }
    console.log('done!')
  })
}

setTimeout(block, 5000)

function ts (gen) {
  if (typeof gen === 'function') gen = gen()
  if (!gen || typeof gen.next !== 'function') return

  (function next () {
    const res = gen.next()
    if (res.done) return
    setTimeout(next)
  })()
}
```

这些代码首先有两个点，第一个点就是我利用 `yield` 关键字，让函数暂停执行，大家都知道在Generator函数中有一个 `yield` 关键字，这个关键字可以让函数暂停执行，这是很关键的特性。我利用的另一个特性就是 `setTimeout` 的能力，它可以将任务丢到宏任务队列里面排队让我的任务恢复执行，所以我结合这两个特性，用这个代码就可以实现Time Slicing的功能。

代码中我下面这个ts函数其实是我封装的工具函数，我上面其实是我的案例。案例中我这个循环其实正常来说是同步的，循环时会把我的浏览器卡死一秒钟，但是我在里面加了一个 `yield` 关键字。所以每次执行都会停一下，停止这一瞬间，其实就是把浏览器的主线程给让出来，或者说叫释放出来了，如果不停的执行，在这一秒钟内浏览器干不了别的事，现在我的这个任务执行了一会就停了，浏览器就可以去执行别的任务。然后我在后面的宏任务中再让我这个任务恢复执行。这个代码可能不是那么好理解，可以自己回去慢慢研究。

>（关于Time Slicing后来我写了一篇文章进行了更详细与全面的介绍，文章地址：[https://github.com/berwin/Blog/issues/38](https://github.com/berwin/Blog/issues/38)）

我这里有一个例子（观看文章的同学可以通过在线PPT来查看视频，地址：[https://ppt.baomitu.com/d/b267a4a3#/19](https://ppt.baomitu.com/d/b267a4a3#/19)），我们会看到浏览器并没有卡死，通过捕获出的火焰图可以看到每个被切割的小任务中间有很多空隙。

## 保证被动交互让用户感觉流畅

现在我们聊下一个话题，**保证被动交互让用户感觉流畅**。

前面我们讲，若想保证被动交互让用户感觉流畅，我们需要保证每16.7毫秒传输新的一帧到屏幕上，所以我们这个标题应该改成 **如何保障动画每16.7毫秒传输新的一帧到屏幕上** 。

这张图是前面我们讲的管道，这个只是图变了一下，若想保证每16.7毫秒传输新的一帧到屏幕上，我们需要保障这个像素管道的总时间在16.7毫秒之内。

![图片](https://p2.ssl.qhimg.com/t01c383a2bcdf417c3f.png)

所以为了保障这个总时间在16.7毫秒之内，我们首先需要保障的事情就是JavaScript的执行时间一定要小于10毫秒，因为浏览器去执行渲染也是有时间消耗的，所以我们应该给浏览器预留出来6.7毫秒。

但其实像素管道的每一步，都有可能导致总时间超过16.7毫秒，所以只是保障JavaScript执行时间小于10毫秒是不够的。我们要针对每一步进行更细致的优化，来保证总时间小于16.7毫秒。

## 更快的样式计算

我们先讨论样式计算，关于样式计算有一个重要的话题是选择器匹配。

### 选择器匹配

![图片](https://uploader.shimo.im/f/dWjv8iyDzxQuhkDg.png!thumbnail)

我们这里有两个选择器，其实选择的是同一个元素，但其实在浏览器里，处理选择器匹配的时候，时间是不一样的，下面更简单的选择器速度更快一点。我在Chrome文档中看到他们说计算某元素的样式时，有50%的时间是用于选择器匹配。

通常如果只是用选择器匹配了一个元素或很少的元素，那么再复杂的选择器，时间上也没有什么太多的影响。但是当选择器匹配到的元素越多的时候，选择器之间的性能差异就体现出来了。

![图片](https://uploader.shimo.im/f/qJrH0jza9F80Sict.png!thumbnail)

下面有三个圈，和三个选择器，我们可以看到第一个选择器是稍微复杂一点的，第二个选择器就是普通的选择器，第三个选择器也比较复杂。我点击这个按纽看三个选择器的执行时间是多少。

![图片](https://uploader.shimo.im/f/B04x0CafQvUTsXDy.png!thumbnail)

可以看到第一个是1.28毫秒，第二个是0.5毫秒，第三个是4.9毫秒，结果虽然在数量上没差太多，但是第三个比第二个慢了9.8倍。

所以我们会发现选择器越简单速度越快，其实这个差距在元素越来越多的情况下，它就会越来越严重，但通常绝大部分的项目其实并没有那么多的元素，所以这个问题也没有暴露的这么明显，了解一下就可以了。

### 布局抖动

第二个问题是布局抖动，它是新手写代码最容易出现的问题，一不小心就犯错了。

我们还是回到像素管道，其实像素管道的每一步都是异步的，js改了样式，其实它是异步的去计算样式，布局，绘制，图层合并，每一步都是异步的。

但是有时候一不小心就会出现一个词叫做强制同步布局，通过这个名就知道，这个布局变成了同步的布局。

![图片](https://uploader.shimo.im/f/NA8nhY2e8Jc0uOJb.png!thumbnail)

浏览器本应是异步的去执行布局操作，但现在却跑到了JS里面去同步的执行了。为什么会导致强制同步布局呢？我们来看一段代码。

![图片](https://uploader.shimo.im/f/RDbQJZ9txYsBp3IP.png!thumbnail)

第一行代码是设置一个元素的宽度，第二行代码是获取元素的宽度，仔细思考一下会发现第一行代码设置了元素的宽，但其实布局操作是异步的，所以我执行第二行代码的时候，浏览器没有还没有进行布局。因为我第二行代码是想获取这个元素的宽，但是这时候浏览器还没有布局，那么浏览器为了回答我这个问题（宽度是多少），它必须要在此时此刻做一次布局，这个时候这个布局是同步的。

![图片](https://uploader.shimo.im/f/f6mSGvKpaggds1GZ.png!thumbnail)

我们将火焰图捕获出来也验证了这一点，布局在我们这个js的里面执行，因为JS里面执行了布局所以把JS的执行时间拉长了。这样是不对的，解决方案很简单，只是调换一下顺序，我如果先获取一个元素出来，其实获取的是上次布局的宽度，我并没有改变布局，所以直接读就可以了，我第二行代码才会改宽度，然后再异步触发布局，这样捕获出来的火焰图布局就跑到JS后面去了。

![图片](https://uploader.shimo.im/f/lgvs9SNZnt884CIz.png!thumbnail)

![图片](https://uploader.shimo.im/f/8N6fmp9qL4MCFnmC.png!thumbnail)

但是通常如果只是这个案例（Demo），其实很简单，你这个再怎么写，也不会有什么问题，因为影响就是很小，但是如果这个问题发生在循环里面，你的元素很多的情况下，这个问题就被放大。

![图片](https://uploader.shimo.im/f/clKXRI450fE2dCXv.png!thumbnail)

这个案例（Demo）也比较简单，代码右边有很多DIV，粉红色的框是这些DIV的父容器，可以看到父容器比这些DIV窄，当我点击“走你~”按钮时，让所有子元素的宽度等于父元素的宽度。（观看文章的同学可以通过在线PPT来操作DEMO，地址：[https://ppt.baomitu.com/d/b267a4a3#/27](https://ppt.baomitu.com/d/b267a4a3#/27)）

通过这个案例（Demo）我们会看到当我点击按钮时，延迟了一会，子元素的宽度才缩小。这是为什么呢？

仔细观察这段代码，我们会发现，循环中的这行代码，其实是两个操作，一个是读取元素的宽度，另一个操作是设置元素的宽度。因为它是在循环里面执行，所以会导致一个现象，每次循环到读取元素宽度时，都会触发一次布局操作。

![图片](https://uploader.shimo.im/f/Rf2hw0uH8soLEJFg.png!thumbnail)

我们来看这张图，当执行 `container.offsetWidth` 时浏览器由于不知道元素的宽度是多少，但我现在马上就要知道这个元素的宽度是多少，所以这个布局不能异步，那么为了告诉我这个元素有多宽，必须马上执行一次同步的布局操作，而随后的代码中又设置了元素的宽度，这其实就是要把刚刚执行的布局给否定掉，让布局失效。当下一轮循环又执行到 `container.offsetWidth` 读取元素的宽时，由于刚刚执行了设置元素的宽，所以浏览器又不知道当前元素的宽度是多少，所以它又要做一次强制同步布局。所以浏览器在不停的布局，让布局失效，布局，让布局失效直到循环结束。

我们将火焰图捕获出来之后，我们会在下面看到一排密密麻麻很多个任务。

![图片](https://uploader.shimo.im/f/wO5dfw2A6QwAJKLd.png!thumbnail)

放大之后是下面这张图，我们可以看到这些任务全是样式计算和布局。这个问题严重就严重在，同一个页面内，两个没有任何关联的元素之间，也会存在这个问题，比如说我的logo改了宽，我再读取其他不相干的元素的宽，两个元素没有任何关系，但是也会有这个影响，只要他们在同一个文档内，所以有时候我们一不小心就会犯错。

解决方案比较简单，就是我把会触发布局的操作踢出去，踢到循环的外面，这时候只读一次宽度，并且由于之前并没有改变任何元素的几何属性，所以浏览器不需要做同步的布局，直接使用之前布局的结果就可以，然后用循环只设置子元素的宽度，就会避免刚才的问题。同样的案例（Demo），只是改了这一行代码，我们点击按钮看一下效果（观看文章的同学可以通过在线PPT来操作DEMO，地址：[https://ppt.baomitu.com/d/b267a4a3#/28](https://ppt.baomitu.com/d/b267a4a3#/28)），已经看不到任何的延迟了。

![图片](https://uploader.shimo.im/f/H13B3NT5e9ssQX6o.png!thumbnail)

![图片](https://uploader.shimo.im/f/HUMk9tIC2pE6MQlu.png!thumbnail)

最终我们捕获出的火焰图就比较正常，就是一个常规的管道应该有的样子，我们先用 js 来触发样式计算，然后浏览器再去布局，再执行绿色的Paint和图层合并，每一步都是异步的。

## 绘制与合成

![图片](https://p4.ssl.qhimg.com/t0192e0a61bb8a6f85f.png)

下一个话题是绘制与合成，你会发现前面我们讲的，就是 JavaScript 和样式计算，还有布局都是单独讲的，但是绘制与合成我们放在一起讲，等下我们再讲为什么。

### 合成

![图片](https://uploader.shimo.im/f/PxhP6ZtkrJUxFxse.png!thumbnail)

我们先讲什么是合成，所谓合成就是浏览器和PhotoShop一样，都有图层的概念，可以看到我这张图最左侧有三个图层，我们从侧面观察这个图层，你会发现眼睛在上面，鼻子在中间，最下面是脸，其实是三个图层是叠加在一起的，这三个图层合并成一张图之后，就是我们最右边的这张图，就是一个人的脸。

**图层有一个最大的特点就是如果图层的位置变了，浏览器只需要重新去合成，就可以得到一张新的图**。注意，如果图层的位置变了，但是图层的内容没变，那么浏览器只需要重新合并图层，就可以得到一张新的图，这个过程是不需要绘制（Paint）的。

### 绘制（Paint）

![图片](https://uploader.shimo.im/f/B8swHH4IqMMv6YX6.png!thumbnail)

我们在说说绘制的意思。图中白色的框是一个图层，这个框里面有一个黄色的方框；右边的与左边的是同一张图层，但是右边这个图层里面的黄色方块跑右边去了。注意，我同一张图层，但是内容变了，这时候浏览器要做一个事情就是“绘制”，通过重新绘制图层，才能让图层里面的内容发生变化。可以理解为，你有一个画板，你想把方框移到右面，那只能把之前的擦掉然后重新在右面画一个上去。

### 添加图层可以取消Paint

**所以你发现绘制产生的效果和图层合并产生的效果是一样的**，我通过改变图层的位置能实现和我重新绘制的效果是一样的。

实际上我想说明什么？我想告诉大家告诉大家添加图层可以取消Paint。

![图片](https://uploader.shimo.im/f/STKUcpmVZDAMFwBn.png!thumbnail)

我们都知道像素管道有五步，JavaScript->样式计算->布局->绘制->合成，但是通过添加图层可以取消绘制这步，五步变成四步，那其实这个时间要更简短一些。

![图片](https://p1.ssl.qhimg.com/t015e150f5dec9d0393.gif)

可以看到这个图，主要看右边的图，就是图层这个位置，这张图的图层在不停的变，浏览器通过合并图层就可以实现方框移动的效果。这个过程不需要绘制的，你用这个火焰图捕获也是捕获不到绘制的。

### 如何创建图层？

图层这么好，如何创建图层？

我们可以使用CSS的will-change来创建图层，在`will-change`不兼容的情况下，你可以用 `transform: translateZ(0);`来代替。

你会发现图层这东西这么好，可以把像素管道从五步变成四步，我们是不是可以这样操作，所有元素都设置`will-change`，浏览器是不是就没有绘制了？

![图片](https://uploader.shimo.im/f/Kf8kOiOUV5wKzxCw.png!thumbnail)

这其实是不行的，因为浏览器做图层管理也是需要消耗的，如果你这样做，其实带来的效果反而是负面的，所以这个是不推荐的。

## 避免丢帧

现在我们从 JavaScript 到图层合并，我们通过一系列的手段已经可以保证每一帧的像素管道总时间在 16.7 毫秒以内，那么就可以保证每 16.7 毫秒给屏幕传输新的一帧吗？

还不够。

图中这是一个时间轴，每个时间节点之间的间隔是 16 毫秒，我们通常会使用Timer触发一个函数改变一些样式，从而实现视觉的效果。

![图片](https://uploader.shimo.im/f/13DenKt1dcYDNkWu.png!thumbnail)

![图片](https://uploader.shimo.im/f/JtTE9pXGdiol0ckq.png!thumbnail)

你会发现中间有一个16毫秒没有输出的，这 16 毫秒丢帧了，这一帧在屏幕上并没有传输任何图像，因为我这个Timer不能保证函数在每一帧最开始执行，保证不了函数的执行频率，所以就会导致这个问题。

![图片](https://uploader.shimo.im/f/uS3gs1QqTa8QeoUe.png!thumbnail)

现在整个Web平台，只有一个API可以解决这个问题，可以让我们的函数在每一帧最开始执行。这个API叫做requestAnimationFrame，使用它触发函数可以保证函数在每一帧的最开始执行，同时只有我们保证函数总体时间在 16.7 毫秒以内，现在就可以下图的效果，我第一帧、第二帧、第三帧、第四帧很均匀，从时间轴上也看不到丢帧的现象存在。现在我们终于可以保证不丢帧的情况下达到 60 FPS。

![图片](https://uploader.shimo.im/f/ZInTx9SFWwwGOd9W.png!thumbnail)

## 总结

![图片](https://uploader.shimo.im/f/QLSu1jVFJkUuO4Hh.png!thumbnail)

最后做一个总结，首先我们讲了什么样的网页是用户觉得比较流畅的，我们讲的第二个概念叫像素管道，通过后面的介绍，你会发现像素管道还是很重要的。

然后我们讲了优化主动交互，有两种方案，一个是web-worker，还有一个是 time-slicing。

我们还介绍了如何优化被动交互，保证 JS 执行时间 10 毫秒以为，样式计算（选择器）与性能，布局抖动以及如何避免布局抖动，做好图层管理和绘制的权衡，和requestAnimationFrame。

谢谢大家。
