进程和线程
- 浏览器进程:负责界面显示、用户交互、子进程管理、提供存储等
- 渲染进程:每个页卡都是单独的渲染进程,核心用于渲染页面
- 网络进程:主要处理网络资源加载(html、css、js)
- GPU进程:3d绘制,提高性能
- 插件进程:浏览器安装的各种插件
从输入url到浏览器渲染页面发生了什么
1浏览器进程的相互调用
- 当用户输入url地址给浏览器,浏览器进程开始工作,进行导航
- 浏览器进程接到任务后就会调用渲染进程,让渲染进程准备着用于渲染页面
- 然后浏览器进程还会调用网络进程加载所需要的资源,网络进程将接收到的资源交给渲染进程处理。
- 渲染显示完毕,进程之间互相通信的过程称为IPC。
- 期间还会使用到插件进程和GPU进程,这2个进程并不会每次全部使用。
2url请求过程
- 网络七层模型 (物 数)底层硬件 网络层(IP) 传输层(tcp 分段传输) (会 表 应)http。
- 查找缓存,检查缓存是否过期,有缓存直接返回缓存
- 查询域名是否被解析过,通过DNS协议将域名解析成对应的IP地址( DNS协议基于UDP )
- 请求如果是https协议,需要SSL协商加密(对称加密、非对称加密)
- 通过IP地址寻址,排队等待。一个IP地址有可能发送多个http请求,最多同时发生6个http请求
- TCP层,传输数据,通过三次握手建立连接。tcp传输数据的特点:可靠有序。
- http请求(包含 请求行、请求头、请求体)
- keep-alive默认开启,为了下次传输数据时,可以复用上次创建的TCP连接
- 服务器接收到数据后, 返回不同类型的状态码;
3HTTP发展历程
- http0.9: 负责传输html,没有请求头 和 响应头
- http1.0: 增加了响应头,可以通过http header的不同解析不同类型的资源
- http1.1: 默认开启了keep-alive,TCP连接复用。 管线化【同时可以建立6个tcp链接】。服务器无法同时处理多个请求,会造成了队头阻塞。
- http2: 用一个tcp链接来发送数据,
- 一个域名只建立一个tcp链接【多路复用】
- 头部压缩,头部header字段可以进行压缩
- 服务器可以推送数据到客户端
- http3: 为了解决http2中一个数据包丢失后,要等待后边包发生完成才能继续重发的问题。http3解决tcp的队头阻塞,采用QUIC协议和UDP协议。
http1实现过程
```javascript // 使用 node 模拟 http1 过程 const http = require(“http”); const path = require(“path”); const fs = require(“fs”); const url = require(“url”);
const staticDir = path.join(dirname, “static”); const server = http.createServer((req, res) => { let { pathname } = url.parse(req.url); if (pathname == “/favicon.ico”) return; console.log(pathname); if (pathname === “/“) { pathname = “/index.html”; fs.createReadStream(path.join(dirname, “index.html”)).pipe(res); } else { let reqUlr = path.join(staticDir, “../“, pathname); try { fs.accessSync(reqUlr); fs.createReadStream(reqUlr).pipe(res); } catch (e) { console.log(e, “err”); res.statusCode = 404; res.end(“not found”); } } }); server.listen(3001, () => { console.log(“server listening on 3001”); });
<a name="WR00x"></a>
#### 使用node模拟http2过程
```javascript
// http2必须依赖https协议
// openssl 生成电子签名
// openssl req -newkey rsa:2048 -nodes -keyout res_private.key -x509 -days 365 -out cert.crt
const http2 = require("http2");
const path = require("path");
const fs = require("fs");
const { HTTP2_HEADER_PATH, HTTP2_HEADER_STATUS } = http2.constants;
const server = http2.createSecureServer({
cert: fs.readFileSync(path.resolve(__dirname, "./cert.crt")),
key: fs.readFileSync(path.resolve(__dirname, "./res_private.key")),
});
const staticDir = path.join(__dirname, "static");
server.on("stream", async (stream, headers) => {
let requestPath = headers[HTTP2_HEADER_PATH];
if (requestPath == "/") {
requestPath = "/index.html";
let dirs = fs.readdirSync(staticDir);
dirs.forEach((dir) => {
let pushPath = path.join(staticDir, dir);
// http2 主动推送资源
stream.pushStream(
{ [HTTP2_HEADER_PATH]: "/" + dir },
(err, pushStream) => {
fs.createReadStream(pushPath).pipe(pushStream);
}
);
});
stream.respondWithFile(path.join(__dirname, requestPath), {
"Content-Type": "text/html",
});
} else {
stream.respond({
[HTTP2_HEADER_STATUS]: 404,
});
stream.end("not found");
}
});
server.listen(3002, () => {
console.log("http2 server run port: 3002");
});
4浏览器接收到资源后,渲染流程
- 浏览器无法直接使用html,需要将html转化成DOM树.(document)
- 浏览器无法解析纯文本的css样式,需要对css进行解析,解析成styleSheets,也称CSSOM(document.styleSheets)
- 计算出DOM树中每个节点的具体样式 (Attachment【复合,连接】的意思)
- 创建渲染树(布局树),将DOM树的可见节点添加到布局树CSSOM中。计算节点渲染到页面的坐标位置(此过程就是 layout 布局)
- 根据布局树进行分层(根据定位属性,透明属性,transform属性,clip属性)生成图层树。
- 将不同图层进行绘制,转交给合成线程处理。最终生成页面,显示到浏览器上【painting,Display】
模拟请求到渲染完成流程
面试题:css为什么放页面header中,js要放在页面底部。 1:css不会阻塞dom解析,只是会阻碍渲染,样式css如果放置到底部,可能会导致重绘 2:js会阻塞dom的解析,也阻塞渲染,影响页面结构的呈现。遇到js脚本时需要暂停dom解析去执行JavaScript,js可能会操作样式和dom结构,所以需要等待样式加载完成后再继续加载js
<head>
<link rel="stylesheet" href="./index.css" />
</head>
<body>
<!-- 浏览器可以部分渲染 -->
<div>123</div>
<!-- css 不会阻塞html解析 -->
<!-- 需要cssom 和 dom tree 生成布局树 -->
<!-- css 会阻塞页面渲染 -->
<!-- parserHTML -> parserStylesheet -> updateLayerTree -> paint -->
</body>
通过浏览器的Performance查看
<head>
</head>
</head>
<body>
<div>123</div>
<!-- 样式放到底部,可能会造成重绘 -->
<link rel="stylesheet" href="./index.css" />
</body>
通过浏览器的Performance查看
<head>
<link rel="stylesheet" href="./index.css" />
</head>
<body>
<div>123</div>
<script>
let s = 0;
for (let i = 0; i < e * 10; i++) {
s += i
}
</script>
<!-- 遇到js脚本时需要暂停dom解析去执行JavaScript,js可能会操作样式和dom结构,所以需要等待样式加载完成后再继续加载js -->
<div>456</div>
</body>
<head>
<link rel="stylesheet" href="./index.css" />
</head>
<body>
<div>123</div>
<script src="./index.js"></script>
<!-- js和css并行加载,要等待js执行完毕,才能继续解析剩余DOM -->
<div>456</div>
</body>
先ParseHtml然后 ParseStylesheet 然后 layout tree,然后paint。
然后执行js代码,
然后再次parseHtml,再次layout tree及paint
MDN文档 - 说明渲染流程
https://developer.mozilla.org/zh-CN/docs/Web/Performance/How_browsers_work#%E6%9E%84%E5%BB%BAdom%E6%A0%91
1基于TCP发送HTTP请求
模拟基于net模块的TCP协议发送http请求,代码实现
请求报文格式
- 起始行:[方法][空格][请求URL][HTTP版本][换行符]
- 首部:[首部名称][:][空格][首部内容][换行符]
- 首部结束:[换行符]
-
响应报文格式
起始行:[HTTP版本][空格][状态码][空格][原因短语][换行符]
- 首部:[首部名称][:][空格][首部内容][换行符]
- 首部结束:[换行符]
实体:
const net = require("net");
class HTTPRequest {
constructor(options = {}) {
this.host = options.host;
this.method = options.method || "GET";
this.path = options.path || "/";
this.port = options.port || 80;
this.headers = options.headers;
}
send() {
return new Promise((resolve, reject) => {
// 构建http请求
const rows = [];
rows.push(`${this.method} ${this.path} HTTP/1.1`);
Object.keys(this.headers).forEach((key) => {
rows.push(`${key}: ${this.headers[key]}`);
});
let data = rows.join("\r\n") + "\r\n\r\n";
// 创建tcp链接,传输http数据
let socket = net.createConnection(
{
host: this.host,
port: this.port,
},
() => {
socket.write(data);
}
);
const parser = new HttpParser();
socket.on("data", (chunk) => {
// console.log(chunk.toString());
parser.parse(chunk);
if (parser.result) {
resolve(parser.result);
}
});
});
}
}
2解析响应结果
请求到资源后,开始解析收到的服务端返回的资源,代码实现
接收到的资源包括responseLine、body、header数据const parser = new HttpParser();
socket.on("data", (chunk) => {
// console.log(chunk.toString());
parser.parse(chunk);
if (parser.result) {
resolve(parser.result);
}
});
3解析html
解析html就是对返回的body数据进行解析,使用 htmlparser2 第三方模块进行html解析。
代码实现
解析html生成DOMTree结构const parser = new HtmlParser.Parser({
onopentag(name, attributes) {
// console.log("start", name, attributes);
let parent = DOMTree[DOMTree.length - 1];
let element = {
tagName: name,
attributes: attributes,
children: [],
parent: parent,
};
parent.children.push(element);
DOMTree.push(element);
},
ontext(text) {
// console.log(text);
let parent = DOMTree[DOMTree.length - 1];
let textNode = {
type: "text",
text,
};
parent.children.push(textNode);
},
onclosetag(name) {
let parent = DOMTree[DOMTree.length - 1];
// console.log(name);
if (name === "style") {
parserCss(parent.children[0].text);
}
DOMTree.pop();
},
});
parser.end(body);
4解析css
使用第三方模块css,对样式文件进行解析。生成stylesheet结构
代码实现5计算样式
function computedCss(element) {
let attrs = element.attributes; //
element.computedStyle = {};
Object.entries(attrs).forEach(([key, value]) => {
cssRules.forEach((rule) => {
let selector = rule.selectors[0];
if (
(selector == "#" + value && key == "id") ||
(selector == "." + value && key == "class")
) {
rule.declarations.forEach(({ property, value }) => {
element.computedStyle[property] = value;
});
}
});
});
}
6布局绘制
update layout tree 和paint阶段
// 浏览器上计算布局绘制的方法
function layout(element) {
//
if ((Object.keys(element.computedStyle).length |= 0)) {
let { background, width, height, top, left } = element.computedStyle;
let code = `
let canvas = document.createElement('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let context = canvas.getContext('2d');
context.fillStyle = "${background}";
context.fillRect(${top}, ${left}, ${parseInt(width)}, ${parseInt(height)})
`;
fs.writeFileSync("./code.js", code);
}
}
总结:DOM如何生成的
通过HTTPRequest请求资源,
- 服务端返回的类型是text/html 时,浏览器会将收到的数据通过 HTMLParser 进行解析。
- 在解析前会执行预解析操作,预先加载js、css文件
- 字节流 -> 分词器 -> tokens -> 根据tokens生成节点 -> 插入到DOM树
- 遇到js,在解析DOM过程遇到script标签,HTMLParser 会停止,下载执行js脚本
- 在js执行之前,需要等待当前脚本上的所有css文件加载解析完毕。js是依赖css的加载。
- css样式文件尽量放在页面头部,css加载不会阻塞DOM Tree解析,浏览器用解析出来的DOMTree和CSSOM进行渲染,不会出现闪动问题。
如果css放在底部,浏览器边解析边渲染,渲染出的结果不包含样式,后续会发生重绘操作 - js文件放在html地步,防止js 的加载、解析、执行阻塞页面的正常渲染。
Performance API
页面加载事件流程图
关键时间节点
| 关键节点 | 描述 | 含义 | | —- | —- | —- | | TTFB | time to first byte | 首字节返回的事件 服务器的处理能力 | | TTI | time to Interactive | 到页面可以交互,花费的时长 | | DCL | DOMContentLoaded | DOMContent加载完成的时间 | | FP | First Paint | 首次绘制的时间 | | FCP | First Contentful Paint | 首次Content内容显示 | | FMP | First Meaning Paint | 有意义内容绘制完成的时间 | | LCP | Largest Contentful Paint | 最大Content内容绘制完成 | | FID | First Input Delay | input输入框延时时间 |
<body>
<!-- 需要等待所有的事件执行完毕后才能计算 -->
<div style="background-color: red;width:100px;height:100px;"></div>
<h1 elementtiming="meaningful">关键内容显示</h1>
<script>
window.addEventListener('DOMContentLoaded', function() {
// let s = 0;
// for (let i = 0; i < 100000000; i++) {
// s += i;
// }
// console.log(s)
setTimeout(() => {
document.body.appendChild(document.createTextNode('hello'))
}, 1000);
})
setTimeout(() => {
const {
fetchStart, // 开始访问
requestStart, // 请求的开始
responseStart, // 响应的开始
responseEnd, // 响应的结束
domInteractive, // dom可交互的时间点
domContentLoadedEventEnd, // dom加载完毕 + domcontentloaded完成的事件的事件 $(function(){})
loadEventStart // 所有资源加载完毕
} = performance.timing;
let TTFB = responseStart - requestStart; // 首字节返回的事件 服务器的处理能力
let TTI = domInteractive - fetchStart; // 整个的一个可交互的时长
let DCL = domContentLoadedEventEnd - fetchStart; // DOM 整个加载完毕
let L = loadEventStart - fetchStart; // 所有资源加载完毕所用的时长
console.log(TTFB, TTI, DCL, L);
const paint = performance.getEntriesByType('paint'); // MDN
console.log(paint[0].startTime); // FP 只是画像素了而已
console.log(paint[1].startTime);// FCP 有内容才行
}, 3000);
// FMP first meaningful paint
// 递归 看load的时间不为0 mutationObserver
new PerformanceObserver((entryList,observer)=>{
console.log(entryList.getEntries()[0]);
observer.disconnect(); // 监控完后直接结束即可
}).observe({entryTypes:['element']});
// LCP
new PerformanceObserver((entryList,observer)=>{
entryList = entryList.getEntries();
console.log(entryList[entryList.length - 1],entryList);
observer.disconnect(); // 监控完后直接结束即可
}).observe({entryTypes:['largest-contentful-paint']});
// FID
new PerformanceObserver((entryList,observer)=>{
firstInput = entryList.getEntries()[0];
if(!firstInput) return
FID = firstInput.processingStart - firstInput.startTime;
console.log(FID)
observer.disconnect(); // 监控完后直接结束即可
}).observe({type:['first-input'],buffered:true});
</script>
</body>
网络优化策略
- 减少http请求次数,合并js、css,使用内嵌css、js
- 设置服务端缓存,提高服务器处理速度,缓存使用【强缓存、协商缓存】
- 避免重定向,重定向会降低响应速度(301 、302)
- 使用dns-prefetch,进行dns 预解析
- 采用域名分片技术,将资源放到不同的域名下,解决同一个域名最多处理6个tcp链接问题
- 采用cdn加速访问速度
gzip压缩优化,对传输资源进行体积压缩
Content-Encoding: gzip
加载数据优先级preload【预先请求当前页面需要的资源】,prefetch【将来页面中使用的资源】,将数据缓存到http缓存中
// 可以设置preload,进行预先加载,设置加载高优先级
<link rel="preload" href="style.css">
关键渲染路径【重要】
重排【回流】reflow:添加元素、删除元素、修改大小、移动元素位置、获取元素位置相关信息
重绘 repaint:页面中元素样式的改变并不影响它在文档流中的位置。
强制同步布局问题
JavaScript强制将计算样式和布局操作提前到当前的任务中 ```javascript function reflow() {
let el = document.getElementById(‘app’); let node = document.createElement(‘h1’); node.innerHTML = ‘hello’; el.appendChild(node); // 强制同步布局 console.log(app.offsetTop); // 获取位置就会导致 重排 (重新布局)
} window.addEventListener(‘load’, function() { reflow(); });
```javascript
console.log(app.offsetTop); // 不再触发布局
function reflow() {
let el = document.getElementById('app');
let node = document.createElement('h1');
node.innerHTML = 'hello';
el.appendChild(node);
// console.log(app.offsetTop);// 强制同步布局
}
window.addEventListener('load', function() {
for (let i = 0; i < 100; i++) {
reflow();
}
});
布局抖动(layout thrashing)
const element = document.getElementById('box');
let start;
function step(timestamp) {
if (start === undefined)
start = timestamp;
const elapsed = timestamp - start;
element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 800) + 'px)';
if (elapsed < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
function sleep(d) {
for (var t = Date.now(); Date.now() - t <= d;);
}
const tasks = [
() => {
console.log("task1");
sleep(10);
},
() => {
console.log("task2");
sleep(10);
},
() => {
console.log("task3");
sleep(10);
},
];
requestIdleCallback(taskLoop, { timeout: 1000 })
function taskLoop(deadline) {
console.log('本帧剩余时间', deadline.timeRemaining());
while ((deadline.timeRemaining() > 1 || deadline.didTimeout) && tasks.length > 0) {
performUnitOfWork();
}
if (tasks.length > 0) {
console.log(`只剩下${deadline.timeRemaining()}ms,时间片到了等待下次空闲时间的调度`);
requestIdleCallback(taskLoop);
}
}
function performUnitOfWork() {
tasks.shift()();
}
减少回流重绘
- 脱离文档流
- 渲染时给图片增加固定宽高
- 尽量使用css3动画
- 可以使用will-change将元素提取到单独图层中
浏览器的渲染
https://www.w3cplus.com/performance/css-rendering-engine.html
静态文件优化
图片优化
图片格式
- jpg
- png
- gif
- webp
-
图片优化
避免空src图片
- 减小图片尺寸,节约用户流量
- img标签设置alt属性,提升图片加载失败时的用户体验
原生 loading:lazy 图片懒加载
<img loading="lazy" src="./images/1.jpg" width="300" height="450">
不同环境下采用不同尺寸和像素的图片
- 对于较大的图片可以考虑采用渐进式图片
- 采用base64 URL图片,减少请求次数
-
html优化
语义化html:代码简洁清晰,利于搜索引擎,便于团队开发
- 提前声明字符编码,让浏览器快速确定如何渲染网页内容
- 减少html嵌套关系、减少DOM节点数量
- 删除多余空格、空行、注释及无用属性
- html减少 iframes 使用,iframe 会阻塞onload 事件
css优化
- 降低样式层级嵌套、减少使用通配符
- 删除多余空格、空行、注释及无用单位,进行css压缩
- 使用外链css,可以对css进行缓存
添加媒体字段,只加载有效的css文件
<link href="index.css" rel="stylesheet" media="screen and (min-width:1024px)" />
可以使用css的contain属性【组件化思想】,将元素进行隔离,可以节约渲染资源
- 减少使用@import使用,@import采用的是串行加载
js优化
- 通过async、defer异步加载js文件。async和defer不阻塞dom解析。async加载完js会立即执行;defer会在最后执行,加载的js有执行顺序要求。
- 减少DOM操作,缓存访问过的元素
- 操作不直接应用到DOM上,应用在虚拟DOM节点,最后一次性应用到DOM
- 使用webworker解决程序阻塞问题
- IntersectionObserver ```html
<script>
const observer = new IntersectionObserver(function(changes) {
changes.forEach(function(element, index) {
if (element.intersectionRatio > 0) {
observer.unobserve(element.target);
element.target.src = element.target.dataset.src;
}
});
});
function initObserver() {
const listItems = document.querySelectorAll('img');
listItems.forEach(function(item) {
observer.observe(item);
});
}
initObserver();
</script>
- 虚拟滚动 virtual-scroll-list
- requestAnimationFrame 、 requestIdleCallback
- ![Frame生命周期.png](https://cdn.nlark.com/yuque/0/2022/png/737887/1648801676474-691edc77-355e-4ac2-a0c6-ea363e402a1a.png#clientId=u7f409bed-ab7d-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=u29a39433&margin=%5Bobject%20Object%5D&name=Frame%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F.png&originHeight=1068&originWidth=2000&originalType=binary&ratio=1&rotation=0&showTitle=false&size=237580&status=done&style=none&taskId=ua5fa5e1c-6230-49de-bd5f-8589efe58b2&title=)
- 避免时eval
- 使用时间委托,减少事件绑定个数
- 尽量使用canvas 动画和css3动画
<a name="ddtOo"></a>
### 图标优化
可以使用字体图标
<a name="xiWRs"></a>
## 核心优化策略
- 关键资源个数越多,首次页面加载时间就越长
- 关键资源的大小,内容越小,下载时间越短
- 优化白屏:内联css和内联js移除文件下载,压缩文件体积
- 预加载,预渲染,打包时进行预渲染
- 使用ssr加速首屏加载,有利于seo
<a name="KgIHO"></a>
## 浏览器存储
- cookie:cookie过期时间内一直有效,存储大小现在4k,同时还限制字段个数,不适合大量的数据存储,每次请求会携带cookie信息,主要用来做身份检查
- 设置cookie有效期
- 根据不同子域 domain 划分 cookie 减少传输
- 静态资源域名和cookie域名采用不同域名,避免静态资源访问时携带的cookie
- localStorage:chrome下最大存储5M,除非手动清除,否则一直存在。利用localStorage存储静态资源。
- sessionStorage:会话级别存储,可用于页面间的传值
- indexDB:浏览器本地数据库,可以用于[渲染大列表时的列表数据](https://gitee.com/shenshuai89/webworkers/blob/master/src/worker.js)
<a name="DWYNB"></a>
## 增加PWA离线缓存(progressive web app)
webapp用户体验差,不能进行离线访问,用户粘性底,pwa就是为了解决这一问题产生的技术,让webapp具有快速、可靠、安全的特点
- web app Mainfest:将网站添加的桌面,类似native的体验
- Service Worker:离线缓存内容,配合cache api
- Push API& Notification Api: 消息推送和提醒
- App Shell & App Skeleton:骨架屏
<a name="QUkuf"></a>
## LightHouse使用
```javascript
npm install lighthouse -g
lighthouse http://www.taobao.com