[浏览器]带你了解页面资源解析

821 阅读6分钟

最近想重新系统地整理一下前端的知识,因此写了这个专栏。我会尽量写一些业务相关的小技巧和前端知识中的重点内容,核心思想。

前言

上一篇文章(点击回顾)我们讲了浏览器页面渲染的流程以及提出一些页面二次渲染的建议。今天我们来看一下浏览器的资源解析过程。

解析白屏

我们发现当浏览器开始渲染页面的时候,会先生成一个空白的页面。然后等到资源的解析,生成完整的页面图片,再显示出来。这个过程的页面就是【解析白屏】。而这个白屏的时间,是我们衡量一个页面的性能标识之一,过长的白屏时间会让用户产生负面的情绪。下面我们就来从资源解析的角度看看可能导致白屏时间增加的以原因有哪些。

解析入口

我们都知道浏览器在渲染一个页面时,需要js,css,html等文件资源。但这些资源并不是一次过来的,当我们在地址栏输入地址发出get请求之后。得到的其实只是一个html资源(当然如果用http/2 还有可能会有其他内容)。浏览器会开始解析这个html文件,因此这个文件是一切渲染动作的入口。

HTML解析

首先浏览器是不需要等HTML文件完全加载完之后才开始解析的,而是网络进程加载了多少数据,HTML 解析器便解析多少数据。这个可以作为额外知识点记忆。

浏览器用会HTML 解析器解析HTML文件,解析过程这里就不作详细解释了,有兴趣的朋友可以自行查阅。大致为解析器会把文件内容根据标签分成一个个的token,由于标签是由起始标签和结束标签的,这样解析器就可以通过在一个栈中处理每个标签中的内容,从而构成一棵dom树。

js,css解析

现在根据上述的知识,一个普通的html文件HTML解析器是可以直接解析的。如下方html文件:

<html>
<body>
    <div>hellop</div>
</body>
</html>

但我们的页面肯定不会这么简单,我们往往还会有css,js等。如下方html文件:

<html>
	<style>
  	div{
    	font-size: 14px;
    }
  </style>
<body>
    <div>hellop</div>
    <script>
    	let div1 = document.getElementsByTagName('div')[0] 
      div1.innerText = 'hello js'
    </script>
    <div>welcome</div>
</body>
</html>

html解析是一个线性的过程,也就是从文件的头部一行行的解析到文件结束。如果遇到了style标签或者script标签html解析器会暂停工作把执行权交给css解析器或者js解析器。解析完该部分之后,又回到html解析器,解析html内容,按这样的步骤重复。

资源下载

如今的web页面与早期的相比功能丰富了许多,出于更好地发开管理我们会把一些css和js分割成单独的文件。因此现在更常见的html文件应该长这样:

<html>
	<link href="theme.css" rel="stylesheet">
<body>
    <div>hellop</div>
    <script src='./helper.js'></script>
    <div>welcome</div>
</body>
</html>

这就意味着浏览器需要额外去下载js和css资源,那么浏览器是不是得逐行解析,解析到link或者script标签之后发现是外部文件,再去下载的呢?

并不是,因为如果这样做效率就太低了。我们知道解析是由渲染进程做的,而下载是网络进程的工作。因此他们其实是可以并发进行的。那么浏览器是怎么处理的呢?原来在html开始正式解析之前,还会有一个预解析操作。这个步骤不是真正去解析html而是扫描分析文件中是否有外部引用文件,如果有的话就会开始下载。

资源阻塞

虽然有了上述的解决方案,却导致了另一些问题,就是【资源阻塞】。

js引起的阻塞

相信大家在日常开发中不难发现一个情况,就是我们在html中写的script如果要操作dom,那么这个dom对应的html标签必须在这个script之前,不然js是找不到它的。

<html lang="en">
<body>
  <script>
    // app 是找不到#app的。
    const app = document.getElementById('app');
    app.innerText = 'hello javascript'
  </script>
  <div id="app">hello world</div>
</body>
</html>
// 控制台报错Uncaught TypeError: Cannot set properties of null (setting 'innerText')

如果把顺序换过来,程序可以正常执行。

<html lang="en">
<body>
  <div id="app">hello world</div>
  <script>
    const app = document.getElementById('app');
    app.innerText = 'hello javascript'
  </script>
</body>
</html>

这也解释了我们为什么推荐把script标签放到最后。同时也说明了html在解析时遇到script标签是会先把执行权交给js解析器,等js执行完之后再解析html的。原因是页面的构建需要DOM树,而js代码可以修改DOM,因此浏览器必须等js直接执行结束之后才继续解析。(尽管js代码中没有操作dom的步骤,浏览器也会默认等待)

那么当js是来自外部的时候,浏览器必须要先等js加载完,再开始解析。这期间渲染进程就会处于空闲状态,造成对解析的阻塞。

css引起的阻塞

同理外部css也会引起同样的阻塞。

两者的依赖关系

上面讲到页面的渲染是需要依赖DOM树和CSSOM树的,而这两者都能被js代码修改。所以页面在解析时,为了确保页面正确渲染,js的解析不仅要等待自身文件的加载完成,还需要确保在此之前的css解析完成。

可以总结我们上述代码的问题可能会有:

  1. 如果css文件的下载比js文件的下载慢,会阻塞js的解析。
  2. 如果js文件的下载比渲染进程html的解析慢,会阻塞html的解析。
  3. 如果css文件的下载比js文件的下载快,但css的解析比js下载慢,同样会阻塞js的解析。

渲染优化

现在我们已经知道了页面渲染可能会遇到的问题,这样我们就可以总结出优化的方案了。通常情况下的瓶颈主要体现在下载 CSS 文件的时间、下载 JavaScript 文件的时间和执行 JavaScript的效率。

因此我们可以总结以下方案:

  • 减少html请求:通过内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了。
  • 压缩资源文件大小:但并不是所有的场合都适合内联,那么还可以尽量减少文件大小,比如通过 webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。
  • 异步执行js:还可以将一些不需要在解析 HTML 阶段使用的 JavaScript 标记上 async 或者 defer。
  • 针对场景选择资源:对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件。

总结

今天我们了解了浏览器的页面解析流程,以及根据流程过可能出现的问题。针对这些问题我们总结了页面渲染优化的方案。希望对大家有所帮助。

参考

《浏览器工作原理与实践》——李兵