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
来观察:
这里的 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>当
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>
标签导致的多次渲染问题。第一次渲染,不论是
Vue
、React
还是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的主要内容,如果未能解决你的问题,请参考以下文章