让你的网页更丝滑(全)

栏目: Html · 发布时间: 4年前

内容简介:大家好,我叫刘博文,今天给大家分享的主题叫《让你的网页更丝滑》,其实就是更流畅的意思。简单介绍一下自己,2012年我从中专毕业,当时是17岁,2015年我加入了360最大的前端团队奇舞团,那一年我是20岁;2017年由于组织架构的变动,我们组被拆分到360导航,所以我就变成360导航的一名前端工程师;2018年就是去年,因为公司是W3C的会员,所以我就加入了W3C的性能工作组。

让你的网页更丝滑(全)

这篇文章是2019年5月11号,我在上海FDConf2019上的分享整理。

  • 演讲主题:【让你的网页更丝滑】
  • 时间:2019年5月11日(下午)
  • 地点:上海 - FDCon2019 - B会场(全栈&全端专场)
  • 演讲嘉宾:刘博文

PPT地址: https://ppt.baomitu.com/d/b267a4a3

让你的网页更丝滑(全)

简介

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

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

让你的网页更丝滑(全)

消息比较灵通的应该听说过我在上个月出版了一本讲Vue的书,叫做 《深入浅出Vue.js》

让你的网页更丝滑(全)

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

什么样的网页是流畅的

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

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

让你的网页更丝滑(全)

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

让你的网页更丝滑(全)

所谓被动交互就是不需要用户主动去触发什么,就可以让网页在视觉上与用户产生交互。

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

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

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

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

让你的网页更丝滑(全)

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

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

让你的网页更丝滑(全)

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

让你的网页更丝滑(全)

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

让你的网页更丝滑(全)

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

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

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

让你的网页更丝滑(全)

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

让你的网页更丝滑(全)

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

让你的网页更丝滑(全)

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

像素管道

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

让你的网页更丝滑(全)

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

避免长任务

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

让你的网页更丝滑(全)

如何避免长任务,有两种方案:一种叫 Web Worker ,还有一种方案叫 Time Slicing(时间切片)。

让你的网页更丝滑(全)

Web Worker

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

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!')
}

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

让你的网页更丝滑(全)

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

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

Time Slicing

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

让你的网页更丝滑(全)

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

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

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后来我写了一篇文章进行了更详细与全面的介绍,文章地址: #38

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

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

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

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

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

让你的网页更丝滑(全)

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

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

更快的样式计算

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

选择器匹配

让你的网页更丝滑(全)

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

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

让你的网页更丝滑(全)

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

让你的网页更丝滑(全)

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

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

布局抖动

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

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

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

让你的网页更丝滑(全)

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

让你的网页更丝滑(全)

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

让你的网页更丝滑(全)

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

让你的网页更丝滑(全)

让你的网页更丝滑(全)

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

让你的网页更丝滑(全)

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

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

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

让你的网页更丝滑(全)

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

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

让你的网页更丝滑(全)

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

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

让你的网页更丝滑(全)

让你的网页更丝滑(全)

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

绘制与合成

让你的网页更丝滑(全)

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

合成

让你的网页更丝滑(全)

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

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

绘制(Paint)

让你的网页更丝滑(全)

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

添加图层可以取消Paint

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

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

让你的网页更丝滑(全)

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

让你的网页更丝滑(全)

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

如何创建图层?

图层这么好,如何创建图层?

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

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

让你的网页更丝滑(全)

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

避免丢帧

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

还不够。

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

让你的网页更丝滑(全)

让你的网页更丝滑(全)

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

让你的网页更丝滑(全)

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

让你的网页更丝滑(全)

总结

让你的网页更丝滑(全)

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

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

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

谢谢大家。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

刘强东自述

刘强东自述

刘强东 / 中信出版集团 / 2016-6-1 / 49.00

京东 1998年,京东还只是中关村一个经营光磁生意的小柜台,月营业额仅有几万元,如今则已经成长为中国营收规模超大的互联网企业,2015年全年营收1813亿,总交易额达到4627亿元; 为解决电商“最后一公里”的痛点,创立并自建B2C物流模式; 经常被争议,却始终坚持“不挣快钱”,选择上市不是因为“缺钱”,只为让合作伙伴睡得着觉,为用户和社会创造价值,由此成就让整个华尔街一片京东红......一起来看看 《刘强东自述》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具