Web性能优化:FOUC

Posted Cubic

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Web性能优化:FOUC相关的知识,希望对你有一定的参考价值。

背景

FOUC,也就是 flash of unstyled content,指的是网页渲染时,外部样式还没加载好,就以浏览器默认样式短暂地展示了部分内容,等到外部样式加载完成,又恢复正常的这个页面闪烁的过程。

看到网上有的文章说现代浏览器已经不需要关注 FOUC 的问题了,这其实是不对的,虽然现代浏览器针对首次绘制做了一些优化,但是代码上的不合理依然可以导致 FOUC 出现。

在这个 SPA 盛行的时代,大部分情况下 FOUC 都不那么容易引起重视,但是有些时候,FOUC 带来的影响仍然是不可忽视的。 举个例子,想象一下,你在黑暗的环境下使用了一个黑暗的主题,打开了一个深色主题的页面,本来是很和谐的,却因为 FOUC 每次都会先在默认的白色背景下闪烁一下,非常影响体验。

原因分析

要了解 FOUC 的原因,首先我们要了解一下浏览器渲染的原理。

浏览器的渲染流程

值得注意的是,整个渲染过程是同步进行的。也就是说,浏览器一边解析 html,一边构建渲染树,构建一部分,就会把当前已有的元素渲染出来。如果这个时候外部样式并没有加载完成,渲染出来的就是浏览器默认样式了。

脚本和样式的执行顺序

  • javascript 会阻塞解析(parser blocking)

    浏览器中的 JavaScript 是在一个线程中执行的,所有的 <script> 都是依次同步执行的。当浏览器解析到一个 <script> 并开始执行时,就会阻塞后面所有的 DOM 构建和渲染。

    一般来说,现代浏览器在阻塞渲染的时候,都会提前加载所需的静态资源,如  CSS  和  JavaScript  脚本,但是此时并不会执行。
  • CSS 会阻塞渲染(render blocking)

    当一个  CSS  尚未加载完成时,浏览器会继续解析和构建  DOM ,但是并不会渲染,因为渲染需要的渲染树是由  DOM  和  CSSOM  共同构建而成的。 因此,这个时候页面的渲染会被阻塞,直到  CSS  加载完成。

性能指标

  • 首次绘制(FP,First paint),表示浏览器渲染任何在视觉上不同于导航前屏幕内容之内容的时间点。

  • 首次内容绘制(FCP,First contentful paint),表示浏览器开始渲染  DOM  内容的时间点。

一般来说,如果 FP 和 FCP 同时发生,页面就不会出现闪烁。当然也有例外,如果 FCP 发生的时候,所需的样式依然没有加载完成,那么 FOUC 依然会出现,这种情况一般发生于,CSS 不是通过 <link> 标签加载的,而是使用 JavaScript 动态插入的。

FP 和 FCP 发生的时机可以通过 Chrome 的 performance 来观察:

Web性能优化:FOUC

这里的 DCL 是 DOMContentLoaded 事件,其他的节点这里就不详细展开了。

绘制的时机

前面已经说过,浏览器的解析和渲染是同步进行的,只要有合适的  DOM  和  CSSOM  构建成了渲染树,就会渲染出来,触发浏览器绘制。 这个过程都是在一个线程中进行,为了优化性能,同步的操作会被合并,只有当所有的同步操作完成后,构建的渲染树才会被渲染。
  • 一个简单的例子:

    对于一个简单  HTML  页面,当  CSS  加载完成,且所有的  DOM  都同步解析完成,才会触发第一次渲染。 也就是说, FP  紧跟在  DCL  后发生。
    <html>
    <head>
    <link rel="stylesheet" href="style.css">
    </head>
    <body>
    <div>hello, world</div>
    </body>
    </html>

    Web性能优化:FOUC

    当 JavaScript 加入之后,就变得不一样了。

  • 当浏览器开始执行一个  <script>  时, DOM  的构建会停下来,因为我们的脚本很可能对当前的  DOM  进行查询和操作。 所以这个时候,就会将已经构建好的渲染树先渲染出来。
    <html>
    <head>
    <link rel="stylesheet" href="style.css">
    </head>
    <body>
    <div>hello, world</div>
    <script ></script>
    </body>
    </html>

    值得一提的是,如果 DOM 树的内容为空,浏览器会直接跳过本次渲染。

    所以对于 SPA,更好的做法是在脚本中去动态创建顶层的容器,而不是写到 HTML 中。如果是在 HTML 先写一个 loading 动画提升体验就另说了。

  • 如果 JavaScript 触发了强制 paint reflow,就会产生更多的绘制,即使 <script> 之前的 DOM 树为空,也有可能使 FP 提前。

  • 多个 <script> 标签放在 <body> 中,会多次触发 paint 。原因和上面说过的一样,每次执行一个 <script> 的时候,浏览器都会暂停 DOM 树的构建,先把当前的渲染树渲染出来。所以如果前面的 <script> 创建了 DOM 元素,后面的 <script> 执行前一定会先触发 paint,如果这时发生样式的变化,就会出现 FOUC

    <html>
    <head>
    <link rel="stylesheet" href="style.css">
    </head>
    <body>
    <div>hello, world</div>
    <script ></script>
    <script ></script>
    <script ></script>
    </body>
    </html>

    可以看到,这里的三个 <script> 标签导致了额外的三次 reflow / paint

    这个问题不容忽视,因为有时候费了很大劲做的优化,一次  Webpack  打包就可以让你前功尽弃。 比如使用了  svg-sprite-loader  之后,把  SVG  图标资源打包到  vendor.js  中,会得到:
    <body>
    <script ></script>
    <script ></script>
    </body>
    在  vendor.js  执行的时候, svg-sprite-loader  会向  <body>  上插入一个大的  <svg> 再到  app.js  执行的时候,就会闪烁了。

解决方案

了解了浏览器绘制的时机, FOUC  的问题就可以迎刃而解了。 这里主要针对  SPA  页面,毕竟对  SSR  的页面来说, FOUC  或许不是一个大问题。
  • 将 JavaScript 资源尽量放到 <head> 中,只保留最后一个包含主逻辑的脚本在 <body> 中,因为它很可能要往 <body> 上挂载元素。这可以解决上面提到的 <script> 标签导致的多次渲染问题。

  • 第一次渲染,不论是 VueReact 还是 VanillaJS,一定要同步放到主逻辑中,确保发生在 DCL 之前。

  • 避免对  DOM  进行不必要的读操作,因为他们会带来的额外的绘制。

参考资料

  • https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/

  • https://developers.google.com/web/fundamentals/performance/user-centric-performance-metrics

  • https://developers.google.com/web/fundamentals/performance/critical-rendering-path/analyzing-crp


以上是关于Web性能优化:FOUC的主要内容,如果未能解决你的问题,请参考以下文章

web开发性能优化---SEO优化篇

web性能优化

技能篇—web性能优化

web性能优化之---JavaScript中的无阻塞加载性能优化方案

Web 性能优化文档及代码编辑器相关的新提案

这些Web前端开发性能优化,你知道吗?