前端渲染如何优化

页面性能包含了服务器请求和响应、加载、执行脚本、渲染、布局和绘制每个像素到屏幕上。这里我们只讨论[渲染、布局和绘制]这个过程。

假设我们通过请求得到了页面加载所需的资源js、css、图片等等,页面开始渲染,主要有以下几步:

  • 浏览器解析 HTML,将接收到的数据 转化为 DOM 树,解析过程中如果发现引用了外部资源则暂停解析,加载外部资源,加载完成后解析剩余HTML
  • 解析Css,构造 CSS 模型,等到 DOM 和 CSSOM 完成之后,浏览器构造渲染树。
  • 计算所有可见内容的样式,一旦渲染树完成布局开始,定义所有渲染树元素的位置和大小。完成之后,页面被渲染到屏幕上。这一步也称为回流重绘

一句话总结: 页面渲染主要分五个步骤:构建DOM -> 构建CSSOM -> 构建渲染树 -> 布局 -> 绘制。

所以当我们更改html中DOM结构或者CSS样式时,都会导致页面从头再渲染一次,并触发回流、重绘。目前市面上的前端框架基虚拟DOM的思路主要就是从这一角度出发,通过虚拟DOM的对比,最小颗粒度的更新真实DOM,减少页面在回流、重绘上耗费的成本。

关于回流&重绘的解释:
  • 回流:在页面中,当我们对元素的宽高大小进行改变时,会触发该元素以及相应的其他元素重新计算布局,这个过程就发生了回流。
  • 重绘:重绘意味着元素只是进行了外观的变化,比如背景色、前景色,但是位置大小不变,这时候元素会重新绘制,相应的也不会对周边的元素产生影响。
    所以我们说回流一定会发生重绘,重绘不一定导致回流,重绘的性能优于回流。

既然清楚了浏览器渲染页面的大致过程,那么我们也可以做一些对应的优化

  • 减少频繁获取dom实例,可以使用变量临时保存。
    因为在浏览器中,DOM的渲染跟JS的执行,分别发生在渲染引擎跟js引擎中,当我们通过JS操作DOM的时候,其实就是在两个线程之间通信,如果操作DOM较为频繁,一来一回对于性能的消耗比较高,并且在DOM的尺寸/外观变化时,还会发生回流、重绘,对于性能的影响就更严重了。
    所以现代化的前端框架都是用了虚拟DOM的概念,通过虚拟dom的对比,使真实DOM的更新更“具体”,

    COPY
    1
    2
    3
    4
    5
    6
    7
    8
    // 优化前
    <script type="text/javascript">
    function test () {
    for(var count = 0; count < 15000; count++){
    document.getElementById('name').innerHTML += 'a';
    }
    }
    </script>
    COPY
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 优化后
    <script type="text/javascript">
    funtcion test(){
    let content = '';
    for(let i = 0; i < 100; i++>){
    content += 'a';
    }
    document.getElementById('name').innerHTML += content;
    }
    </script>
  • 元素的批量修改,设置display:none

  • 大量元素的插入,使用DocumentFragment

  • 对于样式频繁变化的DOM,可以使用决定定位或者相对定位脱离文档流,也可以使用will-change 创建图层,借用 GPU 进行渲染,在兼容性较差的浏览上可以使用3D变形 transform: translateZ(0) 强制创建一个图层。

  • 使用classList来实现对元素样式的修改,而非style,减少触发回流重绘的频次

    COPY
    1
    2
    3
    4
    5
    6
    // 优化前
    <script type="text/javascript">
    const box = document.getElementById('box');
    box.style.height = '100px';
    box.style.width = '200px';
    </script>
    COPY
    1
    2
    3
    4
    5
    // 优化后
    <script type="text/javascript">
    const box = document.getElementById('box');
    box.classList.add('size');
    </script>
  • 避免强制更新布局

一般的,在每次事件循环的末尾会进行一次DOM更新,这一周期大概为16ms,假如我们有以下操作

COPY
1
2
3

box.classList.add('big');
const width = box.offsetWidth;

在为box新增class样式后,我们马上又读取了元素的宽度,而浏览器此时还没有完成渲染,那么浏览器为了计算宽度值,就需要重新发生回流、重绘。
那么正确的做法是先获取宽度,再添加样式:

COPY
1
2
const width = box.offsetWidth;
box.classList.add('big');
  • 对于需要提前加载的资源,可以使用 preload
    COPY
    1
    <link rel="preload" href="/css/mystyles.css" as="style">
  • 对于需要解析的非本站域名/跨域域名,可以使用 dns-prefetch
    • 需要注意的是:
      • dns-prefetch 仅对跨域域上的 DNS 查找有效,因此请避免使用它来指向您的站点或域(避免多此一举)
      • COPY
        1
        <link rel="dns-prefetch" href="https://fonts.googleapis.com/">
  • preconnect
作者: 果汁
文章链接: https://guozhigq.github.io/post/f6d4c315.html
版权声明: All posts on this blog are licensed under the CC BY-NC-SA 4.0 license unless otherwise stated. Please cite 果汁来一杯 !