保证网页应用拥有很高的流畅度是相当重要的,即便只有细微的卡顿,用户也是能够感知到并对应用留下负面的印象。假如卡顿很是严重,那么用户颇有可能会放弃这款应用而寻找其余的选择,这对于辛苦工做已久的整个团队都是很是大的灾难。
css
简单来讲,页面的渲染包含如下这些步骤。 当页面文档抵达浏览器时,浏览器会对文档上的元素进行解析,组成DOM树。在Chrome的DevTools中,这个过程被称为Parse HTML。 接下来DOM会与CSS样式相结合,成为渲染树。在DevTools中,这个过程被称为Recalculate Styles。渲染树和DOM树结构很相似,但又不彻底相同。不会被渲染的元素,好比<head>
和被设置为display: none
的元素,就不会存在与渲染树中。而DOM中不存在的一些元素,例如伪元素,却会被加入到渲染树中。html
知道页面中的元素和CSS的对应关系后,浏览器开始计算每个元素会占用多少空间,以及位于屏幕中的什么位置。这一过程被称为Layout,有时也会被称为Reflow。因为元素之间的布局是彼此影响的,因此该过程可能会很是复杂。git
元素的布局肯定后,浏览器使用光栅器(rasterizer)计算出元素怎样以像素为单位进行展现,这一过程在DevTools中被叫作Paint。对于图片类型的元素,浏览器还须要将图片文件解码到内存中以便显示,这个过程叫作Image Decode,有的时候还会须要对其进行尺寸上的调整,也就是Resize。github
与在Photoshop等图像处理软件中相似,浏览器有时也会将界面分为多个图层,从而免除各图层之间的相互影响。每一个图层中的内容被绘制完毕后,浏览器须要根据图层之间的位置关系,将他们进行整合,这个过程叫作Composite Layers。web
网页的样式并不是是一成不变的。针对用户的操做,页面必须及时给出合理的相应,这意味着页面显示的内容会频繁地发生变化。chrome
一般来讲,页面的变化是由js触发的。在js代码中,会存有对浏览器各类事件的监听以及相应的回调函数,以便对用户的各类操做进行响应。若是这些回调函数中含有对页面布局进行改动的地方,那么接下来就会触发浏览器对页面样式进行从新计算,以后应用更新的样式进行布局,在以后对图层进行绘制,并将它们整合在一块儿。整个过程能够视为一个管道,左侧的事件会致使右侧所有或部分的事件依次发生。浏览器
因此总的来讲,以什么方式进行页面进行修改是相当重要的,修改不一样的样式,浏览器的工做量也是大不相同的。开发者能够参考这一网站,来了解本身的操做对于浏览器来讲都意味着要作哪些工做。bash
若是用一个词来描述网页应用的整我的生(app生),那么最恰当的应该就是LIAR。这倒不是由于它们善于欺骗用户,而是由于这四个字母表明的四个词汇(load-载入、idle-闲置、animations-动画、response-响应),能很好地总结一款应用的生命周期。实际上,这一辈子命周期模型更著名的名称是RAIL。网络
Response
表明对用户操做的响应。及时、准确的响应对于提升用户体验来讲相当重要。通常来讲,假如在用户操做100ms后才提供响应,那么会有被察觉到的轻微延时。而响应须要的时间越长,对于用户体验的破坏程度就越大。app
Idle
表明闲置时间。为了知足更快的加载和相应时间,有些不那么重要的任务须要被延后执行,而用户的闲置时间就是绝佳的机会。页面刚刚加载完以后,用户每每会花必定的时间在当前位置进行浏览(或者仅仅是没反应过来...),这给了咱们加载一些次要资源的时间。但这个过程也不能太长,由于咱们还要保证对于用户操做能够及时地进行响应(100ms内),因此最好将闲置时间内处理的内容划分在50ms内能够完成的片断内,以便在用户操做时能够及时响应。
Animation
就是动画了。目前大部分屏幕设备的刷新频率都在60帧每秒左右,那么在这种状况下,达到60fps(frames per second)的页面刷新频率就是咱们的终极目标,这将让用户彻底相信页面上的变化没有任何不流畅的地方。通过一个很是简单的计算,咱们知道1秒钟显示60帧意味着留给每一帧的渲染时间大约只有16.7毫秒。而假如将浏览器的处理时间考虑在内的话,其实每一帧留给你的时间只有10到12毫秒。在某些状况下,这可能会成为一项比较艰巨的任务。为了实现这一点,有时咱们须要充分利用发出响应前容许的100ms延时,完成一些预处理工做。
FLIP
是由任职于Google的Paul Lewis(事实上这篇文章的不少内容都是引用了他的著做)提出的一种动画实现准则,能帮助动画更容易地达到60fps的流畅度。
FLIP
是Fist
、Last
、Invert
和Play
的缩写,分别表明动画的开始状态、最终状态、翻转,以及播放。总的来讲,FLIP
就是要求你先计算出动画中元素的起止状态,而后把元素直接放置在最终位置上,经过一段“反向”的transition
动画,把元素从起始状态转化到最终状态。
下面是一个示例,用来讲明FLIP具体是如何实现的。线上的DEMO能够点击这里查看。
const el = document.getElementById('el')
// F: First
// 获取元素最初的状态
const positionAtFirst = el.getBoundingClientRect()
const opacityAtFirst = document.defaultView.getComputedStyle(el).opacity
// L: Last
// 获取元素最终的状态
el.classList.add('end')
const positionAtLast = el.getBoundingClientRect()
const opacityAtLast = document.defaultView.getComputedStyle(el).opacity
// I: Invert
// 让元素反转回最初状态
const invertTop = positionAtFirst.top - positionAtLast.top
const invertLeft = positionAtFirst.left - positionAtLast.left
el.style.transform = `translate(${invertTop}px, ${invertLeft}px)`
el.style.opacity = opacityAtFirst
// P: Play
// 等待样式生效,在下一帧再开始过渡动画,不然浏览器将忽略样式的更改,动画会没法显示
requestAnimationFrame(function() {
el.style.transition = 'all 2s'
// 清除反转的位移,从而回到最终状态
el.style.transform = ''
el.style.opacity = opacityAtLast
})
// 当一切结束后,就能够移除动画相关的CSS属性
el.addEventListener('transitionend', () => {
el.style.transition = ''
})
复制代码
FLIP能实现使动画更为流程的缘由是,它将一些代价较高的计算安排在了动画开始前执行。由于从操做到浏览器给出反馈前,用户是能够接受必定时间的延时的(好比100ms),这会是一个很好的进行复杂计算的时机。当计算完成后,使用CSS提供的transition
功能,能以代价很是小的方式完成动画(对位移进行transform
,以及改变opacity
,都只会从新进行composition,而不会触发layout以及paint,这将节省至关大的工做量)。这样就保证了动画一旦开始,就能很是流畅地执行下去。
若是遇到了渲染相关的问题,并想进行细致的分析的话,Chrome的开发者工具(DevTools)会是一个得力的帮手。其中的Performance功能,能详细地展现网页渲染过程当中的每个细节,从而给想进行优化的开发者提供颇有价值的线索。
以刚才的demo为例(由于它很简单,从而查看起来会更加清晰)。在开发者工具的Performance一栏中,点击左上角的录制按钮,刷新页面,再点击Stop按钮结束录制,就能看见从页面开始加载到动画完成的所有浏览器工做细节。
紧贴着是页面的快照,能够在这里看到有一排快照直观地显示了页面的变化过程。假如没有发现这一栏,须要用户在顶部勾选Screenshots功能。
下面的图表是要着重分析的部分,它完整地展现了浏览器在什么时间都进行了什么工做。由于真实的场景下,浏览器进行的任务会是很是密集的,这时你能够点击W
键(或滑动滚轮)放大其中某一部分。图中显示的是js中动画开始的过程,浏览器的主线程(图表中的Main部分)开始了一次Animation Frame Fired
事件,这意味着requestAnimationFrame
方法开始执行。它下方的Function Call
就是做为参数传递的回调函数,点击这个矩形,在下方的Summary栏中能够看到这个函数的具体信息,包括函数名(本例中为匿名函数),在代码中的位置,和函数执行的时间(包括总时间和自身执行时间)。
就像上面提到的管道图中所展现的同样,js执行完以后,每每会跟着从新计算样式、更新布局、重绘、以及合并图层,这些流程均可以在图表中准确地找到对应的执行位置和时间。经过这些图表中的信息,咱们能够很容易地判别浏览器是在哪个步骤耗费了过多的时间从而致使页面的卡顿。
Frames一栏中记录了每一帧的快照,和渲染花费的时间。如以前所讨论过的,咱们要尽力保证每一帧的渲染时间都接近16.7ms,但因为咱们选择在提早进行一些复杂的计算,从而保证后面的动画能够流畅进行,因此目前显示的时间还是在咱们控制范围内的。
若是你勾选了顶部的Memory功能,还能看到内存使用量跟随时间的变化状况,这将帮助你更好地侦测到内存溢出等异常现象。
除了上面介绍的以外,DevTools还有不少其它强大的功能,咱们将在后面具体的实例用进行说明。
现代的js编译器会从新编译咱们的代码,从而使代码的运行速度更快,这一过程是经过即时编译器完成的(Just In Time compiler),它很是庞大而复杂,因此通常的开发者基本没法猜想本身的代码会被编译成什么样子。既然如此,咱们不如放弃一些所谓的微优化(由于他极可能不会按咱们的预期产生理想的效果),把时间花在一些其余能提升页面渲染性能的措施上。
从js的执行时机入手多是个比较好的办法。在某些状况下,浏览器可能正在处理着一些有关样式的工做,但此时出现了一段js代码须要被执行,因而浏览器开始执行这段代码。但这段代码改变了一些页面的样式,因而浏览器以前的工做白作了!它必须重来一遍以前关于样式的工做。若是这是一个脾气很差的浏览器,那么这极可能让它气得丢了一些帧来报复你。
想要浏览器更有效率地工做,避免这一类的返工,咱们须要更好地安排本身的js代码而不是常常去给浏览器添乱,此时requestAnimationFrame
可能会是一个好的选择。
window.requestAnimationFrame
方法告诉浏览器您但愿执行动画,并请求浏览器调用指定的函数在下一次重绘以前更新动画。该方法使用一个回调函数做为参数,这个回调函数会在浏览器重绘以前调用。
使用requestAnimationFrame
的好处是,它会安排js代码尽可能在每一帧的开始时进行,以后才会继续处理跟样是有关的后续工做。这就避免了不知何时出现的js任务致使的返工,从而保证了动画能更流畅地展现。
下面是一个数字增长的动画效果的实现。
// 动画的起始时间
let startTime = null
// 增长显示数额
const increase = (timeStamp) => {
// 第一次执行时,将执行时间设为起始时间
if (!startTime) {
startTime = timeStamp
}
// 计算这一帧距离起始时间的时间差
const timeOffset = timeStamp - startTime
// 若是时间差在两秒内,那么执行动画
if (timeOffset < 2000) {
// 根据时间差计算当前应该增加到多少百分比。开方是为了实现ease-out的效果(想一想它的曲线图)
const percent = Math.pow((timeOffset / 2000), 0.5)
this.setShownAmount(percent)
// 开始下一帧的计算
requestAnimationFrame(increase)
} else {
// 若是动画结束,将数额设置回初始值,防止在最后一帧中出现精度误差
this.setShownAmount()
}
}
// 开始咱们的动画
requestAnimationFrame(increase)
复制代码
能够看到咱们并无像使用setTimeout
或setInterval
同样指定每一帧的间隔时间,这也是使用requestAnimationFrame
的另一个好处,你不须要去管究竟要多久来展现一个帧,浏览器会尽力作到最好。
以前的状况都是比较理想的,js能在很是短的时间内完成,能够经过合理地安排执行时间从而保证动画的流畅执行。但若是遇到了一些须要花费很是久才能够完成的任务,那么不管把它安排到哪里(鉴于js是单线程执行的),它都会毫无疑问地阻塞页面的动画。
此时也许能够考虑帮咱们的主线程找一个帮手,Web Workers是个物美价廉的选择(雇佣他们是免费的!)。
Web Workers能让你在一个彻底独立的上下文中,在一个独立的线程中执行js代码,与主线程互不影响。因而你能够开启一个Web Worker,并将一些耗时很长的任务交给它,等它执行完的时候,再利用它的执行结果进行下一步的操做,从而避免了主线程上的阻塞。
Web Workers的使用很是简单,你只要在须要的时候建立好它,并在它和主线程的代码内部各自作好数据的监听和传递便可。
在主线程内:
// 经过文件来建立一个Web Worker
const myWorker = new Worker('./worker.js')
// 向你的免费劳工传递信息
myWorker.postMessage(msg)
// 作好数据监听的工做,好在它完成任务的时候可以及时响应
myWorker.onmessage = function(e) {
// 在这里你能够充分利用它的劳动成果作任何你想作的事
}
复制代码
在woker.js中:
// 随时等候主人的调遣
this.onmessage = function(e) {
// 主人的命令就藏在e.data中
const msg = e.data
// 不辞辛苦,勤勤恳恳...
// 告诉主人个人工做作完了
postMessage(result)
}
复制代码
怎么样,使唤别人的感受是否是特别的舒畅呢?
有时咱们无心中就会强迫浏览器作了一些它不肯意作的事,既然是不情愿的,过程有时也就不会很顺畅。
下面是一段将全部段落的宽度改成基准宽度的一段代码,它向你展现了如何强迫浏览器作它不情愿的事情。
const ps = document.querySelectorAll('.paragraph')
const benchmark = document.querySelector('.benchmark')
let i = ps.length
// 若是你想和你的浏览器搞好关系 你最好这么写
size = benchmark.offsetWidth
while (i--) {
ps[i].style.width = size + 'px'
}
// 不然你能够强迫浏览器这么作
while (i--) {
ps[i].style.width = benchmark.offsetWidth + 'px'
}
复制代码
若是咱们选择下面这种方式,并且受影响的元素数量(i)很可观的话,浏览器将会在渲染过程当中表现得很是的不情愿。这是由于针对每个元素,咱们都从新获取了一遍基准元素(benchmark)的宽度,而浏览器为了得知这一数据,须要对页面进行从新布局。因此总的来讲,咱们总共从新布局了至少i次,但很明显咱们并没必要要这么作(其实一次就够了)。
在DevTools中咱们能看见,对于一个拥有一千个元素的示例来讲,从新设置他们的宽度使这一帧的渲染时间达到了560.6ms,在实际场景中这是很难被接受的。好在贴心的DevTools给出了明显的提示,右上角标记为红色的部分就是Chrome认为存在异常的部分,而且贴心地给出了提示:
Warning
Forced reflow
is a likely performance bottleneck.
它在提醒你代码中存在强制同步布局(Forced synchronous layout)。
许多获取元素布局信息(好比尺寸、位置)的方法,以及其余一些方法(getComputedStyle
, innerText
,focus
等)都会致使页面从新布局,在代码中咱们都须要尽量地减小执行这些方法的次数,并认真考虑执行他们的时机。具体会触发FSL的方法能够参阅这篇文章。
像以前所提到的,合理地利用分层能提升网页的渲染速度。将一些只会应用transform
和opacity
变换的元素分离到一个独立的层中,能够在渲染时只进行Composite的步骤从而避免浏览器进行额外的工做。那么如何利用这一特性,随时随地把想要的元素抽离到一个图层上呢?
你能够利用will-change: transform
,这个属性告诉浏览器该元素接下来会进行transform的修改,从而提早准备好,将元素放置到一个独立图层中。对于一些还不支持这个属性的浏览器,你能够利用transform: translateZ(0)
实现一样的效果,它伪装要在纵向进行3D转换(实则没有),使浏览器不得不为它建立一个新的图层。
合理地建立图层有时会对页面的渲染速度带来很大的改善,你能够尝试这个来自优达学城的魔性demo来感觉到这一点。进入这个网页后,点击左上角的Animate
按钮,就会开始循环一段诡异的动画。若是你对本身的机器性能不是很自信,那么建议你立刻点击旁边的Isolate
按钮,它会为在这些元素增长一个Z轴方向的位移从而为为其各自建立独立图层,以达到避免把你的浏览器卡到崩溃的结果...
若是想要更细致地观察浏览器都作了什么,能够再次打开DevTools,打开Rendering面板,勾选Paint Flashing和Layer Borders。Paint Flashing功能能够帮你更清晰地查看到哪些元素正在被重绘,假如你没有点击,或是再次点击Isolate按钮,你会发现几乎整个画面都被绿色的区域覆盖着,这意味着他们都在不断地进行重绘。此时若是再次点击Isolate按钮,绿色的区域会消失,意味着再也不有持续的重绘发生。这些元素转而由橙色的方框包裹,这些橙色的方框就是浏览器为它们新建的图层。
若是想知道浏览器具体建立了多少图层,以及这些图层的具体信息,还能够利用DevTools里这个酷炫的功能。在Preformance面板中点击具体的一帧(标记着时间的那一栏),而后选择Layers标签,能够以3D的方式展现各个图层在浏览器上的位置关系。下面是demo中某一帧的图层信息,数不清的图层组成的螺旋形柱体让我想起了一种小时候玩过的玩具。
固然还有不少其余提升浏览器渲染效率的方法没有在本文中进行讨论,但了解以上全部的信息,应该已经能让你在解决渲染性能问题时拥有更多的思路。 事实上这篇文章基本是对优达学城的同名课程的总结和拓展。这是一门很棒的课程,若是你时间充裕,而且更喜欢经过视频进行学习,推荐你也完整地学习一遍。