让骨架屏更快渲染 – xiaOp的博客

时间:2021-1-8 作者:admin

中挪到` 中,HTML 规范允许这样做:

A <link> tag can occur either in the head element or in the body element (or both), depending on whether it has a link type that is body-ok. For example, the stylesheet link type is body-ok, and therefore a <link rel="stylesheet"> is permitted in the body.

这样 CSS 只会阻塞后续内容,骨架屏可以不受影响地被渲染。

<head>
    <style>Skeleton CSS</style>
</head>
<body>
    <div>Skeleton DOM</div>
    <link rel='stylesheet' href='index.css'>
    <div id='app'>...</div>
</body>

但是在 Chrome 中测试后会发现 CSS 依然阻塞渲染,在 Chrome 的关于这个问题的一个讨论中,能看到由于针对这种情况的渲染策略并没有严格的规范,不同浏览器下出现了不同的表现:

  • Chrome 依旧阻塞渲染。Webpagetest
  • IE 符合预期,仅仅阻塞后续内容。Webpagetest
  • Firefox 完全不阻塞渲染,除非 <head> 中已经出现了阻塞的 <link>。这样后续内容就会出现 FOUC。Webpagetest。需要在 <link> 之后加上空的 <script> </script> 达到阻塞后续内容渲染的效果。

在这个长长的讨论中,开发人员试图达到如下效果

  • 任何出现在 <link> 之后的 DOM 内容在样式表加载完成之前都不会被添加到渲染树中,也就是阻塞后续渲染。
  • <link> 增加 async 属性,类似 <script>defer/async,不阻塞渲染,加载完毕立即应用。
  • 由 JS 插入的 <link> 将被异步加载。

通过这种方式,开发者就能让浏览器按照声明顺序,尽快渲染页面内容。开发者 Jake 提出了一个配合 HTTP2 使用的场景

<body>
  <!-- HTTP/2 push this resource, or inline it, whichever's faster -->
  <link rel="stylesheet" href="/site-header.css">
  <header></header>

  <link rel="stylesheet" href="/article.css">
  <main></main>

  <link rel="stylesheet" href="/site-footer.css">
  <footer></footer>
</body>

但是这个功能目前仍然没有在 Chrome 实装,不得不转向其他方法。

异步加载样式表

loadCSS 为异步加载样式表提供了两种方式:

  1. <link ref='preload'>
  2. 提供全局 loadCSS 方法,动态加载指定样式表 我们将使用第一种方法,也是 loadCSS 推荐的方式。

<link ref='preload'> 让浏览器仅仅请求下载样式表,但完成后并不会应用样式,也就不会阻塞浏览器渲染了。如果想在下载完成后应用样式,可以在 onload 回调函数中修改 rel 的值为 stylesheet,像正常阻塞样式表一样应用。 另外,由于浏览器支持度问题,loadCSS 提供了 polyfill (使用 media 属性),以及在不支持 JS 情况下降级。完整例子如下:

<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="path/to/mystylesheet.css"></noscript>
<script>
/*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */
(function(){ ... }());
</script>

我们将在使用了骨架屏的 Vue 项目中应用这种方法。

在 Vue 项目中应用

虽然异步加载的样式表不会阻塞骨架屏的渲染,但是当前端渲染内容替换掉骨架屏内容时,必须保证此时样式表已经加载完毕,否则真正有意义的页面内容将出现 FOUC。由于样式表和 JS 加载顺序无法预知,我们必须考虑两者加载先后的情况。

大致思路

首先必须要保证 Vue 实例在异步样式表加载完毕后进行挂载,如果此时样式还没有完成,我们把挂载方法放到全局,等到样式加载完成后再调用:

app = new App();
window.mountApp = () => {
    app.$mount('#app');
};
if (window.STYLE_READY) {
    window.mountApp();
}

然后使用 <link ref='preload'>,当加载完成时,如果发现全局有 mountApp,就执行挂载:

<link rel='preload' href='index.css' as='style' onload='this.onload=null;this.rel='stylesheet';window.STYLE_READY=1;window.mountApp&&window.mountApp();'>

有了具体思路,下面让我们看看在具体项目中应用时可能遇到的问题。如果不关心具体细节,可以调到“最终效果”一节。

配合 HTMLWebpackPlugin 使用

在生成 SPA 时,通常会使用 HTMLWebpackPlugin,这个插件根据开发者传入的模板生成最终的 HTML,当我们开启了 inject 选项时,会自动插入 <link><script>。在实现上述思路时,需要作出一些修改。

首先,在模板中我们需要加入针对 JS 和 CSS 的 <link ref='preload'>

<head>
    <% for (var jsFilePath of htmlWebpackPlugin.files.js) { %>
        <link rel="preload" href="<%=%20jsFilePath%20%>" as="script">
    <% } %>
    <% for (var cssFilePath of htmlWebpackPlugin.files.css) { %>
        <link rel="preload" href="<%=%20cssFilePath%20%>" as="style" onload="this.onload=null;this.rel='stylesheet';window.STYLE_READY=1;window.mountApp&&window.mountApp();">
        <noscript><link rel="stylesheet" href="<%=%20cssFilePath%20%>"></noscript>
    <% } %>
    <script>
    /*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */
    (function(){ ... }());
    </script>
</head>

然后,由于不需要插件插入 <link>,我们可以编写一个简单的 Webpack 插件,监听 HTMLWebpackPlugin 的事件,过滤掉 CSS。这样插件就不会自动插入 `

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。