• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

JS 和 CSS 是影响页面渲染的

武飞扬头像
小p
帮助1

为什么script脚本要放在body标签的最后面?

这是面试的时候经常遇到的问题,但是很少听到有人能完整的回答出来。其实这个问题并不简单,它涉及到浏览器渲染方面的知识,搞懂了这一块对就能对性能优化有一个比较清晰的认识。下面我将从基础部分一点点的讲透这个问题。

什么是 DOM

当请求页面时,网络传给渲染引擎的是 HTML文件字节流,它是无法直接被渲染引擎理解的,所以要将其转化为渲染引擎能够理解的内部结构。

在渲染引擎内部,有一个叫 HTML 解析器(HTMLParser)  的模块,它的职责就是负责将 HTML 字节流转换为 DOM 结构。

DOM 从 JavaScript 脚本视角来看,DOM 提供给 JavaScript 脚本操作的接口,通过这套接口,JavaScript 可以对 DOM 结构进行访问,从而改变文档的结构、样式和内容

首先看一个问题:HTML 解析器是等整个 HTML 文档加载完成之后开始解析的,还是随着 HTML 文档边加载边解析的?

答案是:HTML 解析器并不是等整个文档加载完成之后再解析的,而是网络进程加载了多少数据,HTML 解析器便解析多少数据。

知道了什么 DOM,那接下来看看 JavaScript 是如何影响 DOM 生成的。

JavaScript 是如何页面渲染的

<html>
<body>
    <div>1</div>
    <script>
    let div1 = document.getElementsByTagName('div')[0]
    div1.innerText = '123'
    </script>
    <div>test</div>
</body>
</html>

HTML 解析器开始解析html字符串,当遇到 script 脚本后,HTML 解析器暂停工作,浏览器会渲染已经解析好了 DOM 结构,即把script 脚本之前的内容进行渲染。

大概意思是,浏览器在解析时,如果遇到了script标签,会先渲染一次这个script标签之前的DOM,然后再去加载和执行js。因为js是可以操作DOM的,如果浏览器不先去渲染一次,js获取的DOM就会是null。比如:

<body>
    <p id="box1">world</p>
    <script>
        console.log(document.getElementById('box1'));
        console.log(document.getElementById('box2'));
    </script>
    <p id="box2">hello</p>
</body>

上面代码中 js 是可以获取到 id 为 box1 的p标签,但是不能获取 box2 的p标签,原因很简单,因为 script 在box2前面。js执行的时候,box2还没有被解析和渲染。

当渲染完成后,JavaScript 引擎介入,并执行 script 标签中的这段脚本,因为这段 JavaScript 脚本修改了 DOM 中第一个 div 中的内容。脚本执行完成之后,HTML 解析器恢复解析过程,继续解析后续的内容,直至生成最终的 DOM。

除了在页面中直接内嵌 JavaScript 脚本之外,我们还通常需要在页面中引入 JavaScript 文件,其实整个执行流程还是一样的,执行到 JavaScript 标签时,暂停整个 DOM 的解析,浏览器渲染 script 脚本之前的DOM结构,最后执行 JavaScript 代码。

不过这里执行 JavaScript 时,需要先下载这段 JavaScript 代码。这里需要重点关注下载环境,因为 JavaScript 文件的下载过程会阻塞 DOM 解析,而通常下载又是非常耗时的,会受到网络环境、JavaScript 文件大小等因素的影响

其实到这里,我们就能部分回答文章的提出的问题了,如果你把 script 放在最前面,同时大部分情况下 js 脚本都是放在服务器中的,因此会阻碍 html 的解析,这是会导致浏览器的白屏。

如果把 script 脚本放在最后面,当遇到 script 脚本时,浏览器其实已经解析完了脚本之前的大部分 html,因此浏览器会渲染出这部分页面,不至于屏幕上什么都没有,导致白屏。

你可以参考下面的代码进行测试:

服务端代码:

const express = require('express')
const fs = require('fs')
const path = require('path')

const app = express()

app.get('/render', (req, res) => {
  // 对js脚本设置延时
  setTimeout(() => {
    fs.readFile(path.join(__dirname, './render.js'), (err, result) => {
      res.setHeader('Content-Type', 'text/javascript')
      res.setHeader('Access-Control-Allow-Origin', 'http://localhost:52330/')
      res.end(result)
    })
  }, 3000)
})

app.listen('8001', () => {
  console.log('running at 8001')
})

前端代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">test</div>
    <script src="http://localhost:8001/render"></script>
    <div id="app1">执行完脚本之后的内容</div>
  </body>
</html>

浏览器页面首先会出现 test 的文字,大约3s后才会出现后面的文字,这充分说明 script 脚本严重阻碍了 html的解析和渲染。

不过 Chrome 浏览器做了很多优化,其中一个主要的优化是预解析操作。当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析 HTML 文件中包含的 JavaScript、CSS 等相关文件,解析到相关文件之后,预解析线程会提前下载这些文件

我们知道引入 JavaScript 线程会阻塞 DOM,不过也有一些相关的策略来规避:

  • 使用 CDN 来加速 JavaScript 文件的加载
  • 压缩 JavaScript 文件的体积
  • 如果 JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载,通过 async 或 defer 来标记代码。async 和 defer 虽然都是异步的,不过还有一些差异,使用 async 标志的脚本文件一旦加载完成,会立即执行;而使用了 defer 标记的脚本文件,需要在 DOMContentLoaded 事件之前执行。

CSS 是如何影响页面渲染的

我们经常被问到 JS 是如何阻碍页面渲染的,很少被问到 CSS 是如何阻碍页面渲染的。那 CSS 会不会阻碍页面的渲染呢?

很多网上的答案都是CSS 不会阻塞页面渲染。真的是这样吗?

我们来做个试验:

服务端代码

app.get('/css', (req, res) => {
  setTimeout(() => {
    fs.readFile(path.join(__dirname, './render.css'), (err, result) => {
      res.setHeader('Content-Type', 'text/css')
      res.setHeader('Access-Control-Allow-Origin', 'http://localhost:52330/')
      res.end(result)
    })
  }, 3000)
})

前端代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="http://localhost:8001/css" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">test</div>
    <div id="app1">124143</div>
  </body>
</html>

我们发现页面需要等3s之后才会有内容显示。这说明 CSS 文件会阻碍页面的渲染。

为什么CSS会阻碍页面的渲染呢?

这就需要从渲染流水线的角度来回答这个问题。首先看这段代码,它分别由 CSS 文件和 HTML 文件构成:

<html>
  <head>
    <link href="theme.css" rel="stylesheet" />
  </head>
  <body>
    <div>com</div>
  </body>
</html>

来分析下打开这段 HTML 文件时的渲染流水线,可以先参考下面这张渲染流水线示意图:

学新通

首先是发起主页面的请求,这个发起请求方可能是渲染进程,也有可能是浏览器进程,发起的请求被送到网络进程中去执行。网络进程接收到返回的 HTML 数据之后,将其发送给渲染进程,渲染进程会解析 HTML 数据并构建 DOM。这里你需要特别注意下,请求 HTML 数据和构建 DOM 中间有一段空闲时间,这个空闲时间有可能成为页面渲染的瓶颈

当渲染进程接收到 HTML 文件字节流时,会先开启一个预解析线程,如果遇到 JavaScript 文件或者 CSS 文件,那么预解析线程会提前下载这些数据。

预解析线程会解析出来一个外部的 theme.css 文件,并发起 theme.css 的下载。这里也有一个空闲时间需要你注意一下,就是在 DOM 构建结束之后、theme.css 文件还未下载完成的这段时间内,渲染流水线无事可做,因为下一步是合成布局树,而合成布局树需要 CSSOM 和 DOM,所以这里需要等待 CSS 加载结束并解析成 CSSOM

那渲染流水线为什么需要 CSSOM 呢?

和 HTML 一样,渲染引擎也是无法直接理解 CSS 文件内容的,所以需要将其解析成渲染引擎能够理解的结构,这个结构就是 CSSOM。和 DOM 一样,CSSOM 也具有两个作用,第一个是提供给 JavaScript 操作样式表的能力,第二个是为布局树的合成提供基础的样式信息。这个 CSSOM 体现在 DOM 中就是document.styleSheets

等 DOM 和 CSSOM 都构建好之后,渲染引擎就会构造布局树。布局树的结构基本上就是复制 DOM 树的结构,不同之处在于 DOM 树中那些不需要显示的元素会被过滤掉,如 display:none 属性的元素、head 标签、script 标签等。

复制好基本的布局树结构之后,渲染引擎会为对应的 DOM 元素选择对应的样式信息,这个过程就是样式计算。样式计算完成之后,渲染引擎还需要计算布局树中每个元素对应的几何位置,这个过程就是计算布局

通过样式计算和计算布局就完成了最终布局树的构建。有了布局树才能进行页面的渲染。

所以 CSS 文件是肯定会阻碍页面的渲染的。

我们再来看看稍微复杂一点的场景,还是看下面这段 HTML 代码:

<html>
  <head>
    <style src="theme.css"></style>
  </head>
  <body>
    <div>1</div>
    <script>
      let div1 = document.getElementsByTagName('div')[0]
      div1.innerText = 'time.geekbang' //需要DOM
      div1.style.color = 'red' //需要CSSOM
    </script>
    <div>test</div>
  </body>
</html>

该示例中,JavaScript 代码出现了 div1.style.color = ‘red' 的语句,它是用来操纵 CSSOM 的,所以在执行 JavaScript 之前,需要先解析 JavaScript 语句之上所有的 CSS 样式

所以如果代码里引用了外部的 CSS 文件,那么在执行 JavaScript 之前,还需要等待外部的 CSS 文件下载完成,并解析生成 CSSOM 对象之后,才能执行 JavaScript 脚本

而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM 的,所以渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载,解析操作,再执行 JavaScript 脚本。

所以说, CSS 在部分情况下也会阻塞 DOM 的生成。

总结下:CSS 不管在有没有脚本的情况下都是会阻碍页面的渲染的。如果没有脚本,因为渲染需要布局树,布局树需要CSS,所以它会影响页面渲染。如果有脚本,因为脚本可能操作CSSOM,所以需要等CSS文件下载完成后才能执行脚本,它其实是阻碍了脚本的执行,间接的阻碍了DOM的解析。

通过对上面的了解,下面来看看这段代码的渲染流程:

<html>
  <head>
    <link href="theme.css" rel="stylesheet" />
  </head>
  <body>
    <div>geekbang com</div>
    <script src="foo.js"></script>
    <div>geekbang com</div>
  </body>
</html>

学新通

在接收到 HTML 数据之后的预解析过程中,HTML 预解析器识别出来了有 CSS 文件和 JavaScript 文件需要下载,然后就同时发起这两个文件的下载请求,需要注意的是,这两个文件的下载过程是重叠的,所以下载时间按照最久的那个文件来算。

不管 CSS 文件和 JavaScript 文件谁先到达,都要先等到 CSS 文件下载完成并生成 CSSOM,然后再执行 JavaScript 脚本,最后再继续构建 DOM,构建布局树,绘制页面。

影响页面展示的因素以及优化策略

那么接下来我们就来看看从发起 URL 请求开始,到首次显示页面的内容,在视觉上经历的三个阶段。

  • 第一个阶段,等请求发出去之后,到提交数据阶段,这时页面展示出来的还是之前页面的内容,参考详解在浏览器中从输入URL到页面展示的过程
  • 第二个阶段,提交数据之后渲染进程会创建一个空白页面,我们通常把这段时间称为解析白屏,并等待 CSS 文件和 JavaScript 文件的加载完成,生成 CSSOM 和 DOM,然后合成布局树,最后还要经过一系列的步骤准备首次渲染。
  • 第三个阶段,等首次渲染完成之后,就开始进入完整页面的生成阶段了,然后页面会一点点被绘制出来。

影响第一个阶段的因素主要是网络或者是服务器处理这块儿。现在重点关注第二个阶段,这个阶段的主要问题是白屏时间,如果白屏时间过久,就会影响到用户体验。为了缩短白屏时间,我们来挨个分析这个阶段的主要任务,包括了解析 HTML、下载 CSS、下载 JavaScript、生成 CSSOM、执行 JavaScript、生成布局树、绘制页面一系列操作。

通常情况下的瓶颈主要体现在下载 CSS 文件、下载 JavaScript 文件和执行 JavaScript。所以要想缩短白屏时长,可以有以下策略:

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

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhckeigb
系列文章
更多 icon
同类精品
更多 icon
继续加载