Rem布局: 从闪屏问题到浏览器渲染原理的探索

955 阅读7分钟

问题出现的背景:移动端 H5 页面引入 rem 布局后,加载页面时会出现元素布局由乱到整齐造成的闪屏问题。在解决问题的过程中,为了搞清问题背后影藏的真相,我逐渐向浏览器的渲染原理发起了浅探索,此文章以解决问题为主,记录浏览器渲染原理的探索路径为辅。

Rem布局是什么?

rem 布局是前端响应式布局实现的一种方式,通过动态设置根元素 font-size 值(一般默认为16px),以此为基准值计算页面上元素的实际大小。

探病因

以下是项目中抽离的外联JS脚本

  (function (doc, win) {
    var docEl = doc.documentElement,
    // 手机旋转事件,大部分手机浏览器都支持 onorientationchange 如果不支持,可以使用原始的 resize
    resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',

    recalc = function () {
        //clientWidth: 获取对象可见内容的宽度,不包括滚动条,不包括边框
        var clientWidth = docEl.clientWidth;
        var $dom = document.createElement('div');
        $dom.style = 'font-size:10px;';
        document.body.appendChild($dom);
        // 计算出放大后的字体
        var scaledFontSize = parseInt(window.getComputedStyle($dom, null).getPropertyValue('font-size'));
        document.body.removeChild($dom);
        // 计算原字体和放大后字体的比例
        var scaleFactor = 10 / scaledFontSize;
        if (!clientWidth) return;
        docEl.style.fontSize = 100*(clientWidth / 375)*scaleFactor + 'px';
    };

    recalc();
    //判断是否支持监听事件 ,不支持则停止

    if (!doc.addEventListener) return;
    //注册翻转事件
    win.addEventListener(resizeEvt, recalc, false);
  })(document, window);
加载过程中出现闪屏问题,我大概列出了几项猜测:
  1. 是否与网络加载延迟导致设置根元素font-size延迟有关?
  2. 根元素font-size是否成功设置?有没有被其他的样式覆盖?
  3. 脚本执行完页面才会开始渲染吗?还是页面的渲染在遇到脚本时先渲染过一次?

开始探索之旅

1. 从浏览器渲染页面的流程下手

这里我贴出一张本人绘制的比较粗糙的流程图(网上有很多标准的流程图,大家可以自行搜索): c75dae7b7b0ecc8de3a7606273c1587.png

2. 构建DOM 树

由于浏览器无法理解使用 html,所以这里我们通过网络请求拿到的 html 会在渲染引擎的 HTML 解析器中被转换成浏览器可以理解的DOM树结构

提问1:

那么现在我可以先思考一下,当这个页面解析的过程中遇到了我的外联 script 脚本,渲染引擎的解析过程会不会发生什么变化?

解答:

渲染引擎的 HTML 解析器会暂停DOM的解析,此时会去加载 JavaScript 代码,然后等待资源加载完成,JavaScript 引擎会去执行这段脚本,脚本执行完后恢复执行DOM解析(同样,内联脚本只是省略了脚本加载的时间,阻塞 DOM 解析一样是会发生的)。为什么这么做呢?我们了解到 DOM 提供给 JavaScript 脚本操作的接口,也就是说我们通过这个可以去修改 DOM 树结构,从而改变文档的结构、样式以及内容,所以脚本后的剩余DOM可能是废弃的,也就没要必要再去解析了。

提问2:

那页面渲染过程遇到脚本执行不是会大大影响渲染性能吗?我们能规避不必要的阻塞 DOM解析吗?

解答:
  1. 我们可以通过将脚本资源部署到CDN节点上,减少资源的加载时间。
  2. 通过 gzip 高压缩的形式减小脚本体积。
  3. 如果我们能明确加载的脚本是没有涉及DOM的修改相关操作的,这里我们可以把脚本改为异步加载,通过设置async 或 defer 来标记,使用方式如下:
  // async: 异步加载脚本,加载完后立即执行
  <script async type="text/javascript" src="../static/js/test.js"></script>
  // defer: 异步加载脚本,加载完后需要等待 DOMContentLoaded 事件之前执行
  <script defer type="text/javascript" src="../static/js/test.js"></script>
3. 样式计算

生成CSSOM: 提供给 JavaScript 操作样式表的能力以及为布局树合成提供基础样式信息。 这里我们得知 JavaScript 有修改 CSSOM 的能力,所以脚本执行前,如果页面中包含外部 CSS 文件或者内联 style 标签时,渲染引擎需要先将内容转换成 CSSOM。

a. 页面上的 CSS: link 引用的 css 文件、style 标签标记的 CSS、元素的属性内嵌的 CSS,转换成浏览器可以理解的 styleSheets 。 b. 标准化样式属性值,我们使用的 rem 就是在这个过程转换成 px 值的,标准化计算后的值更利于渲染引擎理解。 c. 最后计算出 DOM 树中每个节点的具体样式,生成一份 CSSOM Tree。

  <style>
    html {
      font-size: 2rem 
    }
    /* 转变成如下: */
    html {
      font-size: 32px
    }

  </style>
4. 后续的渲染步骤

由于是本着解决问题的态度进行探索,这里对于后续的渲染流程不做过多的介绍,感兴趣的朋友们可以在这个基础上更深层次的探索,步骤大致如下:

  • 创建布局树:渲染引擎根据 DOM 树以及 CSSOM 相关的节点信息,构建一颗只包含可见元素的布局树。
  • 布局计算:计算布局树中节点的坐标位置。
  • 分层、图层绘制、栅格化、合成和显示。至此我们的页面就完成渲染到屏幕上了

回到文章开头的问题:加载页面时会出现整体元素由小到达的闪屏问题

阐述现象:

一开始考虑到代码的重用性(会在大量的 H5 页面中引用),我将动态计算 REM 脚本作为外部脚本引入页面放在底部。页面加载会出现闪屏。尝试把网络调成3G模式,能显而易见的观察到页面渲染了两次。以下是我的项目代码: image.png 页面加载效果: image.png

image.png

分析原因

遇到外联脚本时,DOM 解析暂停执行,由于外联脚本请求耗时原因,浏览器处于空闲状态,此时渲染引擎会做两件事:1. 判断是否有加载完的 CSS , 有则解析。2. 判断是否有可渲染的 DOM,有则进入渲染过程。所以这里我们的页面会先进行一次页面渲染,此时根元素 font-size 值是未设置成功的,导致页面布局紊乱,这块跟我们上文中探病因的猜测倒是相呼应了。等待我们的外联脚本加载完,脚本执行计算根元素基准值大小,页面会重新进行一次重排渲染显示正常页面。

申明:本人仅仅是对现象产生的一种合理分析,这块具体浏览器内部是如何处理的可能需要在源码上下功夫,如果有深入探索的,也欢迎分享一下相关资料呀!

解决方案

  1. 改成内联脚本,脚本会在很短的时间内执行完成,页面不会先渲染一次,可以解决问题,但是这里要注意设置根元素的脚本一定要在其余外联脚本上,否则同样的会出现上面的现象,原因就是上述分析。
  2. 如果考虑到脚本重用性,要抽离成外联引用的话,那咱这里是不是就可以考虑把脚本放在页面开头,就算浏览器遇到外联脚本会事先渲染一次,但是页面上没有任何元素可渲染,不过这里需要考虑到一个页面白屏问题,如何取舍就看需求需要了。
  3. 不过还有一种解决方案是从 CSS 的媒体查询着手,直接通过媒体查询的方式将常见的机型设置根元素值,不过有局限性,代码如下:
  @media (min-width: 320px){html{font-size: 84.2667px;} }
  @media (min-width: 360px){html{font-size: 96px;} }
  @media (min-width: 375px){html{font-size: 100px;} }
  @media (min-width: 384px){html{font-size: 102.4px;} }
  @media (min-width: 414px){html{font-size: 110.4px;} }
  @media (min-width: 448px){html{font-size: 119.466px;} }
  @media (min-width: 480px){html{font-size: 128px;} }
  @media (min-width: 512px){html{font-size: 136.53px;} }
  @media (min-width: 544px){html{font-size: 145.066px;} }
  @media (min-width: 576px){html{font-size: 153.6px;} }
  @media (min-width: 608px){html{font-size: 162.1334px;} }
  @media (min-width: 640px){html{font-size: 170.6666px;} }
  @media (min-width: 750px){html{font-size: 200px;} }

结尾:本文初尝由浅入稍深的方式去输出一篇技术文章

本篇文章主要还是从解决问题为主,对浏览器的原理做了浅探索,有了一个大概的认知,背后还有很多精彩的地方,后续还会对整个知识脉络进行一次梳理,相信疏通这块的知识在我们平时的开发也会带来事半功倍的效果。

参考文章链接