公司的一个桌面应用,electron开发,目前是自己在负责. 优化方案前前后后花了两周多. 优化级别对标的是vscode, 都说vscode用了很多黑魔法, 那到底黑魔法是指什么呢, 今天先带着大家扒一扒他的首页...
vscode首页performace性能指标
从chrome的performace控制台入手,打开控制台如下
DCL / FP
从上图时间轴中Screenshots可看到大概58ms左右骨架屏显示出来,这个时间点对应的是DCL/FP,即DomContextLoad事件触发并且开始第一次绘制. 所以我们项目的目标就是DCL&FP优化到50-70ms
先简单说下上图五颜六色的意义. 蓝色:Parse Html 所有的蓝色(无论是cpu轴还是main线程中的蓝色)都代表了html的解析. 黄色: Evluate Script 代表js的执行 紫色: Recalculate Style 样式计算
放大这段时间轴,我们看看这期间发生了什么:
- HTML Parser: 在解析期间可以看出在蓝色中间是加载了wokbench.js, js阻塞了ParseHTML. 这段js加载和执行后继续html的解析. 中间这段js大概阻塞了20多ms.
- Recalculate Style 样式计算
- 之后相继触发了DCL和FP
源码分析workbench.html
D:\Microsoft VS Code\resources\app\out\vs\code\electron-sandbox\workbench\workbench.html
html
<!-- Copyright (C) Microsoft Corporation. All rights reserved. -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta
http-equiv="Content-Security-Policy"
content="
default-src
'none'
;
img-src
'self'
data:
blob:
vscode-remote-resource:
vscode-managed-remote-resource:
https:
;
media-src
'self'
;
frame-src
'self'
vscode-webview:
;
script-src
'self'
'unsafe-eval'
blob:
;
style-src
'self'
'unsafe-inline'
;
connect-src
'self'
https:
ws:
;
font-src
'self'
vscode-remote-resource:
vscode-managed-remote-resource:
https://*.vscode-unpkg.net
;
require-trusted-types-for
'script'
;
trusted-types
amdLoader
cellRendererEditorText
defaultWorkerFactory
diffEditorWidget
diffReview
domLineBreaksComputer
dompurify
editorGhostText
editorViewLayer
notebookRenderer
stickyScrollViewLayer
tokenizeToString
;
"/>
</head>
<body aria-label="">
</body>
<!-- Startup (do not modify order of script tags!) -->
<script src="workbench.js"></script>
</html>
总结: 可以看出head里面竟然没有link css. 而 只有一个script 是放在body下面.
问题1. 为什么head里面不写link css
问题2. 为什么script放在body后面, 而不用defer或async
针对第一个问题,我的理解是 css的加载和解析虽然不影响HTML解析,但script的加载会阻塞HTML解析,而CSS解析会阻塞script的执行(不影响scrpt的解析).所以css间接地阻塞了HTML解析.
为什么要这样设计呢? 其实是因为浏览器遇到script标签时会同步加载并执行对应的js文件, 但它并不知道js中的代码会干些什么,js可以去改动DOM,也可以获取/改变css样式。js要获取正确的样式就必须等css加载完
还有一个问题为什么不用async和defer呢
script对页面加载的影响
async vs defer vs module
参考mdn
async vs defer
- 共同点是 script的加载都是异步的,不会阻塞HTML的解析.
- 区别是,他们的执行是否会中断HTML的解析
- async是下载结束立即执行, 此时如果HTML解析没有结束,那么就阻塞了HTML的解析,还有一种情况,如果下载比较慢可能script执行的时候,HTML已经解析结束
- defer是等HTML解析完全结束后再执行
- defer是按出现的位置顺序执行,async是谁先下载好谁先执行
这里提一下mdn对DOMContentLoaded事件的定义
当 HTML 文档完全解析,且所有deferred脚本(
<script defer src="...">
和<script type="module">
)下载和执行完毕后,会触发 DOMContentLoaded 事件。它不会等待images, subframes,和 async scripts 的加载。DOMContentLoaded 不会等待stylesheets的加载,但deferred script 会等待样式表,而且 DOMContentLoaded 事件排在deferred script之后。此外,不是deferred 或async的脚本(如
<script>
)也会等stylesheets的加载。
再提一下<script type="module">
,他和defer,async的区别呢
所以defer属性对模块脚本也不会生效------它们默认是 defer 的。
其次如果同时设置defer和async,async生效.
script写在body标签下还推荐吗
先说答案, 推荐. 所vscode页面上看,就是script写在body下面
为了解释这个原因,先说说浏览器解析html的几个特性
- 浏览器解析HTML时,虽然会一行一行向下解析,但是它会预先加载具有引用标记的外部资源(例如带有src标记的script、link标签),而在解析到此标签时,则无需再去加载,直接运行,以此提高运行效率。
- 解析HTML时,如果遇到了script标签,会先渲染一次这个script标签之前的DOM,然后再去加载和执行js。因为js是可以操作DOM的,如果浏览器不先去渲染一次,那么js获取的DOM就会是null。所以script放在body后面会强制html的渲染.此时会先触发FP事件. 让页面更快地让用户看到
综上: body下的script就是defer的区别就是defer会等浏览器全部解析完才会执行.
stackoverflow.com/questions/1...
stackoverflow.com/questions/4...
link标签对script的影响
看vscode首页的源码,是没有写link标签的, why
link标签会阻塞页面的渲染
- html和css的解析虽然互不影响, 即dom和css树的构建互不影响, 但合成渲染树的时候需要等待dom树和css树都构建完成. 所以css的解析(包括link外部样式表和内部样式表)会阻塞页面的渲染
这是为了确保页面在渲染时应用了正确的样式, 避免出现无内容闪烁,FOUC ,Flash of Unstyled Content
- link和script也有一个相同的特性, 就是浏览遇到link标签的时候会立即把之前已解析的内容渲染出来. link不会阻塞前面内容渲染,只会影响后面的内容
- 注意: 页面head是中的scipt和link是不会有页面提前渲染的特性. 因为默认head是没有布局的
link会阻塞script的执行
link不会阻塞script的加载,但会阻塞script的执行, script又会阻塞页面的渲染
可能是这种考虑,vscode没有用link标签把
vscode在DCL/FP之前发生了什么
从上面的截图可以看到,在DCL/FP之前,页面的背景色竟然已经渲染好了. 这里是针对Chrome做了什么优化吗, 其实没有什么. 是因为electron在创建窗口的时候可以设置一个背景色, 所以vscode中我们看不见白屏
www.electronjs.org/docs/latest...
www.electronjs.org/docs/latest...
理论验证
如何验证上面的一些结论
自己写一个node 服务器, 设置js和css的请求时间.然后打开浏览器的性能分析即可验证
server.js
js
const http = require("http");
const fs = require("fs");
const path = require("path");
const hostname = "127.0.0.1";
const port = 80;
const server = http.createServer((req, res) => {
console.log(req.url);
if (req.url === "/index.js") {
setTimeout(() => {
const stream = fs.createReadStream(path.join(__dirname, "index.js"));
stream.pipe(res);
}, 1000);
return;
} else if (req.url === "/style.css") {
setTimeout(() => {
const styleStream = fs.createReadStream(path.join(__dirname, "style.css"));
styleStream.pipe(res);
},2000);
return;
} else {
fs.readFile(path.join(__dirname, "index.html"), (err, data) => {
if (err) {
res.statusCode = 500;
res.setHeader("Content-Type", "text/plain");
res.end("Error loading index.html");
} else {
res.statusCode = 200;
res.setHeader("Content-Type", "text/html");
res.end(data);
}
});
}
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
index.html
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>script demo</title>
<!-- <script src="./jquery-git.js"></script> -->
<!-- <script defer src="./jquery-git.js"> </script> -->
<!-- 251kb -->
</head>
<body>
<div style="display: flex;width: 90dvw;height: 40dvh;background-color: bisque;">
</div>
<!-- <script src="./index.js" > </script> -->
<link rel="stylesheet" href="style.css">
<div style="display: flex;width: 90dvw;height: 40dvh;background-color:darkorange;" >
</div>
</body>
</html>
index.js
js
console.log('js start')
for (let index = 0; index < 1000000000; index++) {
}
console.log('js end')
style.css
js
body{
background-color: red;
padding: 5px;
}
developer.chrome.com/docs/devtoo...