如何写出高性能网页

时间:2021-05-04 23:58:21

如何写出高性能网页

  很多时候,我们可以在进入某些网站时,发现其响应非常的缓慢,如何不是自身的网速问题,那么很有可能就是网站自身的性能问题了。 

 

第一部分:网页是如何生成的

  网页生成的过程可以分为下面几个过程:

  1. HTML/SVG/XHTML代码转化成DOM Tree
  2. CSS代码转化成CSSOM,即CSS Object Model。
  3. JavaScript脚本通过DOM APICSSOM API分别来操作DOM Tree和CSS Object Model。
  4. 结合DOM和CSSOM,生成一颗渲染树(包含每个节点的视觉信息)。显然这里的渲染数是不同于DOM树的,比如元素的属性为display:none;的元素就不会出现在渲染树上。
  5. 生成布局(layout),也就是flow的过程,它会将所有渲染树的所有节点进行平面合成,但这是还只是生成布局。
  6. 布局绘制(paint)在屏幕上。

  在这6个步骤中,前面的4步的速度都非常快,只有后面两步,即生成布局(flow)绘制(paint)是非常耗时的,这两个过程合成为“渲染”(render)。

  

第二部分:重排和重绘

  我们一定听过重排(reflow)和重绘(repaint)这两个词,但是它们是什么,区别在哪呢?

  之前说到,flow和paint合成渲染(render),应当知道,下面这三种情况会导致网页重新渲染

  1. 修改DOM
  2. 修改样式表
  3. 用户事件(比如鼠标点击、输入框输入文字等)

  重新渲染,就需要重新生成布局和重新绘制。前者成为reflow(重排),后者称为repaint(重绘)。值得注意的是:

  1.重排一定会触发重绘,因为重排是先于重绘发生的。

  2.重绘不一定会触发重排,比如只是改变某个元素的颜色,布局不会改变,就不会重排,只有重绘。

 

  

第三部分:重新渲染对于性能的影响

  第一部分已经提到:重新渲染是最为耗时的过程,所以如果想要提高网页性能,就需要尽量少地触发重新渲染。

  值得注意的是:虽然DOM和CSS的变动会导致重新渲染,但是浏览器并不会DOM和CSS变动一次就重新渲染一次,而是尽量将所有的变动集中在一起,排成队列,然后一次性执行,这样重新渲染的次数就会大大减少。举例如下:

element.style.color = 'red';

element.style.marginTop = '30px';

这里虽然对于element有两次变动,按照固有的思路即执行了第一条,那么会使其颜色发生改变,发生耗时的重绘,接着js执行第二条语句,布局发生了改变即发生重排,进而导致了重绘,但事实上,浏览器并不是这样乖乖地执行操作的,而是接受到第一个重新渲染的命令之后,并没有立即执行,而是等待(非常短暂的时间)观察下一个命令是否是重新渲染,如果是,就将它们都排成一个队列(FIFO),直到最后一个命令不再是重新渲染,就一次性执行,这样就避免了多次的重新渲染导致的耗时问题。

  但是上面浏览器自行将命令排成队列是有前提条件的,即读操作和写操作不能放在同一条语句中。如下:

element.style.color='red';

element.style.marginTop = (parseInt(element.style.marginTop)+10) + "px"  ; 

  在这两条语句中,因为第二条中出现了读操作,所以读操作就阻断了队列,使得第一个命令立即导致重新渲染,第二个命令在读取后发生另一次重新渲染。但是如果我们先执行了读操作,再执行剩下的两个写操作就有可能会避免这样的问题出现,如:

var marginTop = parseInt(element.style.marginTop);

element.style.color = 'red';

element.style.marginTop =marginTop + "px"  ; 

  这样,本身读操作是不会导致重新渲染的,而后面的两个写操作会放进一个队列中只会导致一次重新渲染。

  也就是说,我们应当尽量将写操作时需要用到的读操作尽可能全部放在前面、放在一起,而写操作放在后面使其加入一个队列。但是这仍不是最好的办法,下面会介绍到。

  

第四部分:提高性能的九个技巧  

  1. 之前提到过的,DOM的多个读操作或写操作应当放在一起,不要在两个读操作之间,加入一个写操作。

  2. 如果某个样式是通过重排得到的,那么最好缓存结果,避免下一次用的时候,浏览器又要重排。 

  3. js中不要一条一条地改变样式,这是最蠢的方式,而是尽量通过改变class,如果不能改变class,就改变cssText属性,这样就可以一次性的改变样式。如下所示:

// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top  = top  + "px";

// good 
el.className += " theclassname";

// good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";

 

   我们可以看到使用el.style.cssText是非常强大的,只是需要使用+来拼接字符串会显得有些麻烦。而className的方式可以很好的体现出结构和样式分离的最佳实践。

  4. 尽量使用离线DOM,而不是真实的网页DOM,来改变元素样式,比如,先操作Document Fragment(碎片,孤儿)对象,完成后再把这个对象加入DOM,再比如使用cloneNode()方法,再克隆的节点上进行操作,然后再用克隆的节点替换原始节点。

  5. 先将元素设置位display:none;(需要一次重排和重绘),然后这个元素就不会出现在渲染树上了,此时对这个节点进行100次操作,最后再恢复显示(又需要一次重排和重绘),但是这样一来,你就仅仅使用了两次重新渲染取代了可能高达100次的重新渲染!

  6. position 属性设置为absolute或fixed的元素,重排的开销会比较小,因为不用考虑它对其他元素的影响

  7. 只有在必要的时候,才将元素的display属性设置位可见,因为不可见元素不影响重排和重绘,另外,visibility:hidden的元素只会对重绘有影响,不影响重排。

  8.使用虚拟DOM的脚本库,比如React等。

  9. 使用window.requestAnimationFrame()、window.requestIdleCallback()这两个方法调节重新渲染。

  

 

第五部分:刷新率

  scroll事件的回调函数和网页动画就会导致密集的刷新率。网页动画的每一帧(frame)都是一次重新渲染。 每秒低于24帧的动画,人眼就能感受到停顿,一般的网页动画,需要达到30帧到60帧的频率,才能比较流畅。大多数显示器的刷新频率是60Hz,这也就意味着如果网页动画能够做到每秒钟60帧,就会跟显示器同步刷新,达到最佳的视觉效果,这就需要在一秒内60次重新渲染,每次重新渲染的时间不能超过16.66毫秒。

  而一秒之内能够完成多少次重新渲染,这个指标就被称为“刷新率”,英文为FPS(frame per second),60次重新渲染,就是60FPS。

  如果想达到60帧的刷新率,就意味着JavaScript线程每个任务的耗时,必须少于16毫秒。一个解决办法是使用Web Worker,主线程只用于UI渲染,然后跟UI渲染不相干的任务,都放在Worker线程。

    

 

 

第六部分:开发者工具的Timeline面板

  Timeline面板是查看刷新率的最佳工具。

 

更多关于Timeline和动画制作提高流畅性的技巧看这里