【浏览器渲染原理系列】这是一份关于加载script和style的实验报告
❝如果做性能优化,一定会想当的一个优化点就是script标签和link标签要放置的位置,当然大部分的观点都是script标签放到</body>之前,link标签放到title中;或者是配合async、defer、preload、prefech使用,当然目的只有一个:让页面尽可能快的展示在用户面前。下面仅仅会讨论浏览器获取到HTML文件后的部分。
❞
浏览器渲染过程
❝浏览器获取到HTML文件后,开始渲染工作。这里以webkit引擎为例。
解析html产生DOM树 解析css样式产生CSSOM树 DOM树和CSSOM树合成渲染树(RenderTree) 布局RenderTree(layout):确定在屏幕中位置 绘制(paint) 合成(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)
DCL(DOMContentLoaded): 当HTML被完全加载以及「解析」时,DOMContentLoaded事件会被触发,而不必等待样式表,图片或者子框架完成加载
L(load): 当整个页面及所有依赖资源如「样式表和图片都已完成加载」时,将触发load事件
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>
❝总结:
❞
内联的script会阻塞dom解析,并且不会使之前解析过的dom预先渲染 外部引入的script标签会阻塞dom的解析,但是之前解析过的dom浏览器会先渲染 加入async和defer可以强制script标签不去阻塞dom的解析 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>
❝总结:
❞
css不会阻塞dom的解析,但是会阻塞dom的渲染 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>
❝总结:
❞
css不会阻塞外部脚本的加载,但是会阻塞js的执行(GUI线程和js线程互斥,因为有可能js会操作 CSS) 最佳实践:script标签放在body的末尾,style标签放在body之前
参考文档
https://developers.google.cn/web/fundamentals/performance/critical-rendering-path https://developers.google.cn/web/fundamentals/performance/rendering