进程和线程

- 浏览器进程:负责界面显示、用户交互、子进程管理、提供存储等
- 渲染进程:每个页卡都是单独的渲染进程,核心用于渲染页面
- 网络进程:主要处理网络资源加载(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.crtconst 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'); // MDNconsole.log(paint[0].startTime); // FP 只是画像素了而已console.log(paint[1].startTime);// FCP 有内容才行}, 3000);// FMP first meaningful paint// 递归 看load的时间不为0 mutationObservernew PerformanceObserver((entryList,observer)=>{console.log(entryList.getEntries()[0]);observer.disconnect(); // 监控完后直接结束即可}).observe({entryTypes:['element']});// LCPnew PerformanceObserver((entryList,observer)=>{entryList = entryList.getEntries();console.log(entryList[entryList.length - 1],entryList);observer.disconnect(); // 监控完后直接结束即可}).observe({entryTypes:['largest-contentful-paint']});// FIDnew PerformanceObserver((entryList,observer)=>{firstInput = entryList.getEntries()[0];if(!firstInput) returnFID = 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(); });
```javascriptconsole.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- - 避免时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使用```javascriptnpm install lighthouse -glighthouse http://www.taobao.com
