浏览器渲染过程和CRP优化二:CRP优化

1,131 阅读5分钟

上一篇文章解释了浏览器的渲染过程,简单说了一下什么是CRP,这篇文章来记录一下如何进行CRP优化

浏览器渲染(解析)页面的过程如下:

  1. 导航:输入URL、DNS解析、建立连接
  2. 响应:发送请求、接收第一次响应的HTML文本
  3. 解析:构建DOM、构建CSSOM、编译javascript
  4. 渲染:样式(Style,构建Render树)、布局计算(Layout,回流/重排)、绘制(Paint,重绘)。

其中解析和渲染步骤比较重要

我们将解析和渲染的步骤中关键的五步提取出来,作为浏览器的关键渲染路径,优化关键渲染路径可提高渲染性能

这五步分别是

  1. 第一步是处理HTML标记,构建DOM树
  2. 第二步是处理CSS,构建CSSOM树。
  3. 第三步是将DOM和CSSOM组合成一个Render树。
  4. 第四步是在依据渲染树计算每个节点的大小和位置。
  5. 最后一步是根据渲染树和回流得到的几何信息,得到节点的绝对像素,将各个节点绘制到屏幕上。

image。png

即:构建DOM->构建CSSOM->构建Render树->回流->重绘。

详细参考上一篇文章 浏览器渲染过程

CRP优化之-解析阶段优化

构建DOM树时遇到样式

构建DOM树时遇到样式,有以下几种情况 image。png

  1. 遇到style内嵌样式,GUI直接渲染

    • 所以项目中如果CSS代码量比较少,直接内嵌即可,拉取HTML的时候,同时CSS也回来了,渲染的时候直接就渲染了
    • 但是如果CSS代码比较多,如果还用内嵌,一方面会影响HTML的拉取速度,也不利于代码的维护,此时还是用外链的方式比较好
  2. 遇到link标签(异步),浏览器开辟一个HTTP线程去请求资源文件信息,同时GUI继续向下渲染(一边下载一边渲染,等下载好了再继续把css拿到了然后渲染)

    tips:浏览器同时能够发送的HTTP请求是有数量限制的(谷歌:5~7个),超过最大并发限制的HTTP请求需要排队等待,所以HTTP请求一定是越少越好。

  3. 遇到@import(同步),浏览器也是开辟HTTP线程去请求资源,但是此时GUI也暂定了(导入式样式会阻碍GUI的渲染),当资源请求回来之后,GUI才能继续渲染

    所以真实项目中应该避免使用@import

构建DOM树时遇到js

遇到 <script src='xxx/xxx.js'> ,有三种解析的方式,分别是 默认,async,defer,默认情况下会阻碍GUI的渲染

如图所示三种方式的渲染规则:

image。png

(图片中, 'net' 表示请求js文件 , 'execution' 表示执行(解析或者叫渲染)js)

如果不想阻碍GUI渲染,该怎么办?在标签里加属性 async defer

  • defer:请求JS资源是异步的(单独开辟HTTP去请求),不会阻碍GUI渲染,当GUI完全渲染完,才会把请求回来的JS去渲染。
  • async:请求JS资源是异步的(单独开辟HTTP去请求),此时GUI继续渲染;但是一但当JS请求回来,会立即暂停GUI的处理,接下来去渲染JS

js文件的依赖关系:

  • 假如我们有多个JS的请求,如果不设置任何属性(默认情况下),肯定是按照顺序请求和执行JS的(依赖关系是有效的)
  • 但是如果设置async,谁先请求回来就先渲染谁,依赖关系是无效的
  • 如果使用defer是可以建立依赖关系的。浏览器内部在GUI渲染完成后,等待所有设置defer的资源都请求回来,再按照编写的依赖顺序去加载渲染js。只有script标签写在浏览器的开头或者中间才需要加上defer,不然加defer写在浏览器结尾和默认什么都不写方式一样

image。png image。png

tips:遇到img,音视频,也是和link的机制一样,异步加载,继续渲染GUI。

总结

所以真实项目中开发是:

  1. link标签放在头部:一般把link放在页面的头部(是为了在没有渲染DOM的时候,就通知HTTP去请求CSS了,这样DOM渲染玩,CSS也差不多回来了,更有效的利用时间,提高页面的渲染速度)
  2. script标签放在底部:我们一般把JS放在页面的底部,防止其阻碍GUI的渲染,如果不放在底部,我们最好设置上async/defer;

解析阶段优化方案

根据以上原理的具体优化方案:

  1. 标签语义化和避免深层次嵌套,加快构建DOM树的过程。

  2. 因为CSS选择器渲染是从右到左的,所以css选择器不要过长,加快构建CSSOM的过程。

  3. 将style、link标签你放到顶部。style里的css直接渲染,link标签利用HTTP多请求并发机制,link标签异步请求,同时继续解析DOM,可以尽早地把CSS下载。避免使用@import,因为是同步,需要等待

  4. 将script标签放到底部,或者使用async、defer属性避免阻塞的JS加载

CRP优化之-渲染阶段优化

渲染阶段主要优化回流和重绘

虽然现在项目都用vue和react来写,DOM操作已经被极大的简化,很少考虑DOM操作的性能问题,但是当我们自己封装不依赖框架的功能性组件插件的时候,性能仍然是不可忽视的问题

DOM的重绘和回流Repaint & Reflow

回流:元素的大小或者位置发生了变化(当页面布局和几何信息发生变化的时候) , 触发了重新布局,导致渲染树重新计算布局。 如

  1. 添加或删除可见的DOM元素;
  2. 元素的位置或尺寸发生变化;
  3. 内容发生变化(比如文本变化或图片被另一个不同尺寸的图片所替代);
  4. 页面一开始渲染的时候(这个无法避免);
  5. 因为回流是根据视口的大小来计算元素的位置和大小的,所以浏览器的窗口尺寸变化也会引发回流

重绘:元素样式的改变(但宽高、大小、位置等不变)。如 outlinevisibilitycolorbackground-color

注意:回流-定会触发重绘。而重绘不一定会回流

避免DOM的回流重绘

  1. 放弃传统操作dom的时代,基于vue/react使用数据影响视图模式。

    mwm/ mvc / virtual dom/ dom diff

  2. css集中改变

    举例:

     <head>
         <style>
         #box {
                 background: lightcoral;
                 width: 500px;
                 height: 50px;
             }
         </style>
     </head>
     <body>
         <div id="box"></div>
         <script src="test.js"></script>
     </body>
    

    test.js

    let box = document.querySelector('#box');
    box.style.width = '100px';
    box.style.height = '100px';
    

    如果直接这样写,不会出现样式改变的过程,因为在DOM解析的时候,最后会执行js,执行完js再构建DOM树,这时候box的样式已经变为100px,所以以上代码,仅仅会执行一次回流和重绘,就是最开始的那一次。

    如果我们这样写:

    test.js

    setTimeout(()=>{
        let box = document.querySelector('#box');
        box.style.width = '100px';
        box.style.height = '100px';
    },1000)
    

    那么这样会有两次回流和重绘,第一次是页面刚加载完成,第二次是1s之后重新改变。

    以上操作在老版本的浏览器中会改变两次回流重绘,因为改变了两次样式

    这样写将样式集中改变可以减少次数

    box.style.cssText = "width:100px;height:100px;";
    

    而新版浏览器不会重绘两次,新版浏览器有一个渲染队列机制。接下来再详细说明

  3. 分离css读写操作( 现代的浏览器都有渲染队列的机制)

    连续改变两次样式,在新版浏览器不会重绘两次。新版浏览器有一个渲染队列机制,会把所有要改变的样式依次放在渲染队列里面,然后把渲染队列中的样式渲染一次。

    当修改样式的代码已经没有了或者遇到了获取元素样式的代码,都会刷新渲染队列:即把现有队列中的样式去统一修改和渲染一次,引发一次回流和重绘 image。png

    获取样式的方式:

    • style.xxx
    • getComputedStyle
    • getBoundingClientRect
    • offsetTopoffsetLeftoffsetWidthoffsetHeightclientTopclientLeftclientWidthclientHeightscrollTopscrollLeftscrollWidthscrollHeightgetComputedStylecurrentStyle ...

    以下会引起两次回流和重绘 image。png

    如果有一个需求,让其在原始宽度的基础上加100

    box.style.width = parseFloat(window.getComputedStyle(box)['width']) + 100 + 'px';
    box.style.height =  parseFloat(window.getComputedStyle(box)['height']) + 100 + 'px';
    

    改为:

    let prevW = parseFloat(window.getComputedStyle(box)['width']),
        prevH = parseFloat(window.getComputedStyle(box)['height']);0
    box.style.width = prevW + 100 + 'px';
    box.style.height = prevH + 100 + 'px';
    
  4. 元素批量修改

    使用文档碎片减少回流: createDocumentFragment

    以下会触发十次回流与重绘

    let box = document.querySelector('#box'),
        for (let i = 0; i < 10; i++) {
            let span = document.createElement('span');
            span.innerHTML = i + 1;
            box.appendChild(span);
        }
    

    改为

    let box = document.querySelector('#box'),
            frag = document.createDocumentFragment();//文档碎片
        for (let i = 0; i < 10; i++) {
            let span = document.createElement('span');
            span.innerHTML = i + 1;
            frag.appendChild(span);
        }
        box.appendChild(frag)
    

    或者用模板字符串拼接方式:

    let box = document.querySelector('#box'),
        str =  `  ` ;
    for (let i = 0; i < 10; i++) {
        str +=  ` <span>${i+1}</span> ` ;
    }
    box.innerHTML = str;
    
  5. 动画效果应用到position属性为absolute或fixed的元素上(脱离文档流)

    因为在重绘的时候,是分层重绘的,每一个文档流单独进行重绘,所以将动画效果单独放到一个文档流上面,可以减少性能开销

  6. CSS3硬件加速( GPU加速)

    比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘; transform \ opacity \ filters 这些属性会触发硬件加速,不会引发回流和重绘 可能会引发的坑:过多使用会占用大量内存,性能消耗严重、有时候会导致字体模糊等

  7. 牺牲平滑度换取速度

    每次1像素移动一个动画,但是如果此动画使用了100%的CPU,动画就会看上去是跳动的,因为浏览器正在与更新回流做斗争。每次移动3像素可能看起来平滑度低了,但它不会导致CPU在较慢的机器中抖动

利用渲染队列机制做一个轮播图

写一个轮播图,原理是将第一张图复制一个放在最后,然后再运动到最后的时候,无过渡跳转到第一张图,再继续运行轮播图。代码如下:

<style>
    * {
        margin: 0;
        padding: 0;
    }

    .container {
        position: relative;
        margin: 20px auto;
        width: 800px;
        height: 300px;
        overflow: hidden;
    }

    .container .wrapper {
        position: absolute;
        top: 0;
        left: 0;
        z-index: 10;
        display: flex;
        justify-content: flex-start;
        align-items: center;
        width: 4000px;
        height: 100%;

        /* 动画 */
        transition: left .3s linear 0s;
    }

    .container .wrapper .slide {
        width: 800px;
        height: 100%;
    }

    .container .wrapper .slide img {
        display: block;
        width: 100%;
        height: 100%;
    }
</style>
<div class="container">
    <div class="wrapper">
        <div class="slide">
            <img src="images/1.jpg" alt="">
        </div>
        <div class="slide">
            <img src="images/2.jpg" alt="">
        </div>
        <div class="slide">
            <img src="images/3.jpg" alt="">
        </div>
        <div class="slide">
            <img src="images/4.jpg" alt="">
        </div>
        <!-- 克隆 -->
        <div class="slide">
            <img src="images/1.jpg" alt="">
        </div>
    </div>
</div>
let container = document.querySelector('.container'),
    wrapper = container.querySelector('.wrapper'),
    step = 0,
    timer;

timer = setInterval(function () {
    step++;
    if (step >= 5) {
        // 立即回到第一张
        wrapper.style.transition = 'left 0s';
        wrapper.style.left =  `0px` ;
        // 运动到第二张
        step = 1;
    }
    wrapper.style.transition = 'left .3s';
    wrapper.style.left =  `-${step*800}px` ;
}, 2000);

但是这样运行后会有问题,发现运行到最后的时候,突然跳到第二涨,并且还存在过渡。

原因是与渲染队列机制有关系,当运行step运行到5,浏览器会把四句样式修改放到渲染队列中,最后执行一次回流重绘,这样的话就只有最后两句css起作用了,就从最后一张直接拉到第二张了,而且还有过渡动画。 image。png

所以我需要让让其立即刷新一下渲染队列,先跳到第一章,再从第一张过渡到第二张。

let container = document.querySelector('.container'),
    wrapper = container.querySelector('.wrapper'),
    step = 0,
    timer;

timer = setInterval(function () {
    step++;
    if (step >= 5) {
        // 立即回到第一张
        wrapper.style.transition = 'left 0s';
        wrapper.style.left =   `0px`  ;
        // 运动到第二张
        step = 1;
        // 刷新渲染队列
        wrapper.offsetLeft;
    }
    wrapper.style.transition = 'left .3s';
    wrapper.style.left =   `-${step*800}px`;
}, 2000);

image.png

手动刷新渲染队列

如果不利用这个机制,那么只能这样写

image.png