【浏览器渲染原理系列】这是一份关于加载script和style的实验报告

共 11852字,需浏览 24分钟

 ·

2021-07-07 11:07

如果做性能优化,一定会想当的一个优化点就是script标签和link标签要放置的位置,当然大部分的观点都是script标签放到</body>之前,link标签放到title中;或者是配合async、defer、preload、prefech使用,当然目的只有一个:让页面尽可能快的展示在用户面前。下面仅仅会讨论浏览器获取到HTML文件后的部分。

浏览器渲染过程

浏览器获取到HTML文件后,开始渲染工作。这里以webkit引擎为例。

  1. 解析html产生DOM树
  2. 解析css样式产生CSSOM树
  3. DOM树和CSSOM树合成渲染树(RenderTree)
  4. 布局RenderTree(layout):确定在屏幕中位置
  5. 绘制(paint)
  6. 合成(composite): 将多个图层合并

为了提高用户体验,渲染引擎会尽快的把结果渲染给用户,它不会等待所有的html都解析完成才渲染,它会在网络层获取文档的同时,将已经接收的局部渲染到页面中(实验4将会证明这一说法)

实验环境准备

1. 模拟服务端: 所需要的文件hand.js、style.css、server.js

// server.js
const http = require('http');
const fs = require('fs')

http.createServer(function (request, response{

  if (request.url === '/index.html') {
    fs.readFile('./index.html', (err, data) => {
      response.setHeader('Content-Type''text/html');
      if (!err) {
          response.end(data);
      }else {
          response.end('html not found');
      }
    })
  }

  if (request.url === '/hand.js') {
    fs.readFile('./static/hand.js', (err, data) => {
      response.setHeader('Content-Type''text/javascript');
      if (!err) {
          setTimeout(() => {
            response.end(data);
          }, 100);
      }else {
          response.end('html not found');
      }
    })
  }

  if (request.url === '/style.css') {
    fs.readFile('./static/style.css', (err, data) => {
      response.setHeader('Content-Type''text/css');
      if (!err) {
          setTimeout(() => {
            response.end(data);
          }, 1000);
      }else {
          response.end('html not found');
      }
    })
  }

  if (request.url === '/favicon.ico') {
    response.end()
  }

}).listen(8888);

console.log('port 8888')

// hand.js(无内容)
// style.css
p {
  color: red;
}

前端部分

<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>
  <p>我是第一个</p>
  <p>我是第二个</p>
  <p>我是第三个</p>
</body>
</html>

备注:以下的实验图片中,请忽略掉实验1、3、8、9、10报告的第一个渲染帧(因为浏览器会纪录页面刷新前的一个渲染帧)

script标签

为什么script标签会阻塞页面的渲染?

javascript能操作dom树,浏览器却不知道脚本中是否有操作dom的代码(比如document.write等),所以以最坏的打算来处理:停止dom的解析,所以更准确的说是「script标签会阻止dom的解析」

解释几个下面实验用到的名词(以下解释均来源于MDN)

  1. DCL(DOMContentLoaded): 当HTML被完全加载以及「解析」时,DOMContentLoaded事件会被触发,而不必等待样式表,图片或者子框架完成加载

  2. L(load): 当整个页面及所有依赖资源如「样式表和图片都已完成加载」时,将触发load事件

  3. FP(first paint): 页面导航与浏览器将该网页的第一个像素「渲染」到屏幕上

以上名词缩写将会出现在下面的实验截图中

实验1: 内联script标签

<head>
  <meta charset="UTF-8">
  <script>
    var a = 0
    for (let i = 0; i< 1000000000; i++) {
      a += 1
    }
  
</script>
</head>
<body>
  <p>我是第一个</p>
</body>
<head>
  <meta charset="UTF-8">
</head>
<body>
  <p>我是第一个</p>
  <script>
    var a = 0
    for (let i = 0; i< 1000000000; i++) {
      a += 1
    }
  
</script>
</body>

实验2: 内联script标签(换一种实现方式)

<head>
  <meta charset="UTF-8">
</head>
<body>
  <script>
    const node = document.getElementsByTagName('p')
    console.log(node[0]) // undefined
  
</script>
  <p>我是第一个</p>
</body>

<head>
  <meta charset="UTF-8">
</head>
<body>
  <p>我是第一个</p>
  <script>
    const node = document.getElementsByTagName('p')
    console.log(node[0]) // <p>我是第一个</p>
  
</script>
</body>

实验3: 外部引入的script标签放在title中

<html lang="en">
<head>
  <meta charset="UTF-8">
  <script src="http://localhost:8888/hand.js"></script>
</head>
<body>
  <p>我是第一个</p>
</body>
</html>

实验4: 外部引入的script标签放在body中

<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>
  <p>我是第一个</p>
  <p>我是第二个</p>
  <script src="http://localhost:8888/hand.js"></script>
  <p>我是第三个</p>
</body>
</html>

实验5: 外部引入的script标签放在</body>前

<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>
  <p>我是第一个</p>
  <p>我是第二个</p>
  <p>我是第三个</p>
  <script src="http://localhost:8888/hand.js"></script>
</body>
</html>

实验6: 外部引入的script标签放在title中,并且加入async参数

<head>
  <meta charset="UTF-8">
  <script src="http://localhost:8888/hand.js" async></script>
</head>
<body>
  <p>我是第一个</p>
  <p>我是第二个</p>
  <p>我是第三个</p>
</body>

实验7: 外部引入的script标签放在title中,并且加入defer参数

<head>
  <meta charset="UTF-8">
  <script src="http://localhost:8888/hand.js" defer></script>
</head>
<body>
  <p>我是第一个</p>
  <p>我是第二个</p>
  <p>我是第三个</p>
</body>

总结:

  1. 内联的script会阻塞dom解析,并且不会使之前解析过的dom预先渲染
  2. 外部引入的script标签会阻塞dom的解析,但是之前解析过的dom浏览器会先渲染
  3. 加入async和defer可以强制script标签不去阻塞dom的解析
  4. defer会阻塞DOMContentLoaded事件,而async不会

link标签引入的css

我们一般用link标签引用css样式文件。如果你看过vue打包后的文件,会发现它的一些脚本文件也是通过link标签引入的。不过我们这篇文章中不对其进行讨论。「样式文件不会阻塞dom的解析,但是会阻塞dom的渲染」,接下来用几个实验来证明css是如何阻塞dom的渲染的

实验8: link标签引入的css放到title中

<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="http://localhost:8888/style.css">
</head>
<body>
  <p>我是第一个</p>
  <p>我是第二个</p>
  <p>我是第三个</p>
</body>

实验9: link标签引入的css放到</body>前

<head>
  <meta charset="UTF-8">
</head>
<body>
  <p>我是第一个</p>
  <p>我是第二个</p>
  <p>我是第三个</p>
  <link rel="stylesheet" href="http://localhost:8888/style.css">
</body>

总结:

  1. css不会阻塞dom的解析,但是会阻塞dom的渲染
  2. css若放在body末尾,页面会从无样式到有样式的过渡,会让用户体验很差

script与css的标签同时存在

一个静态文件中一定会同时存在script和link标签的情况,它们之间又是互相影响的?

实验10: link与script都放在title中,且link放在script之前

<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="http://localhost:8888/style.css" />
  <script src="http://localhost:8888/hand.js"></script>
</head>
<body>
  <p>我是第一个</p>
  <p>我是第二个</p>
  <p>我是第三个</p>
</body>

实验11: link与script都放在title中,且link放在script之后

<html lang="en">
<head>
  <meta charset="UTF-8">
  <script src="http://localhost:8888/hand.js"></script>
  <link rel="stylesheet" href="http://localhost:8888/style.css" />
</head>
<body>
  <p>我是第一个</p>
  <p>我是第二个</p>
  <p>我是第三个</p>
</body>

实验12: link在title中,script放到</body>前(情况1)

<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="http://localhost:8888/style.css" />
</head>
<body>
  <p>我是第一个</p>
  <p>我是第二个</p>
  <p>我是第三个</p>
  <script src="http://localhost:8888/hand.js"></script>
</body>

实验13: link在title中,script放到</body>前(情况2)

// 让我们修改一下serve.js文件中的js文件和css的返回时间
if (request.url === '/hand.js') {
    fs.readFile('./static/hand.js', (err, data) => {
      response.setHeader('Content-Type''text/javascript');
      if (!err) {
          setTimeout(() => {
            response.end(data);
          }, 1000);
      }else {
          response.end('html not found');
      }
    })
  }

  if (request.url === '/style.css') {
    fs.readFile('./static/style.css', (err, data) => {
      response.setHeader('Content-Type''text/css');
      if (!err) {
          setTimeout(() => {
            response.end(data);
          }, 100);
      }else {
          response.end('html not found');
      }
    })
  }
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="http://localhost:8888/style.css" />
</head>
<body>
  <p>我是第一个</p>
  <p>我是第二个</p>
  <p>我是第三个</p>
  <script src="http://localhost:8888/hand.js"></script>
</body>

总结:

  1. css不会阻塞外部脚本的加载,但是会阻塞js的执行(GUI线程和js线程互斥,因为有可能js会操作 CSS)
  2. 最佳实践:script标签放在body的末尾,style标签放在body之前

参考文档

  • https://developers.google.cn/web/fundamentals/performance/critical-rendering-path
  • https://developers.google.cn/web/fundamentals/performance/rendering

浏览 56
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报