浏览器渲染原理 1

进程与线程

  • 进程是操作系统资源分配的基本单位,进程种包含线程。
  • 线程是由进程所管理的,为了提升浏览器的稳定性和安全性,浏览器采用了多进程模型。

在这里插入图片描述

  • 浏览器进程: 负责界面显示,用户交互,子进程管理,提供存储等
  • 渲染进程:每个页面都有单独的渲染进程,核心用于渲染页面
  • 网络进程:主要处理网络资源加载(HTML, CSS ,JS等)
  • GPU进程:3d绘制,提高性能。
  • 插件进程:chrome种安装的一些插件。

从输入url到浏览器显示,发生了什么?

从浏览器进程看:
  • 输入url或者关键字。如果是关键字,根据默认的引擎生成地址。(浏览器进程里面做的事情)
  • 浏览器进程,准备一个渲染进程,用于渲染页面
  • 网络进程加载资源,最终将加载的资源交给渲染进程处理
  • 渲染完毕显示。
网络七层模型:物理层 数据链路层 网络层(ip) 传输层(tcp udp) (会话层,表示层,应用层)
  • 输入url后,先查找缓存,检测缓存是否过期,没过期直接返回缓存内容。
  • 看域名是否被解析过,本地缓存,dns解析,将域名解析成ip地址(DNS基于UDP) ip+端口号 host
  • 请求如果是htpps (ssl协商)
  • ip地址来进行寻址,排队等待,最多能发送6个http请求
  • tcp创建连接,用于传输(三次握手)
  • 利用tcp传输数据(拆分成数据包, 有序)可靠,有序,服务器会按照顺序来接受
  • http请求(请求行 请求头 请求体)
  • 默认不会断开, Keep-alive,为了下次传输数据的时候,可以复用上次创建的连接。
  • 服务器收到请求后,返回数据(响应行 响应头 响应体)
  • 服务器返回301, 30进行重定向操作(重新重投开始走下来)
  • 服务器返回304,查询浏览器缓存并且返回。
浏览器接受资源后:

在这里插入图片描述

  • 1 浏览器无法直接使用HTML,需要将HMTL转成DOM树(document)
  • 2 浏览器无法解析纯文本的css样式,需要对css进行解析城styleSheets(css样式表)。CSSDOM(document.styleSheets)
  • 3 根据dom树和css对象计算出DOM树种每个节点的具体样式(Attachment)
  • 4 创建渲染(render tree),将DOM树种可见节点,添加到render tree中,并且计算每个节点渲染到页面的坐标位置(即layout阶段)
  • 5 通过render tree,进行分层(根据定位属性,透明属性,transform属性,clip属性)生成图层树
  • 6 将不同图层进行绘制(painting),转交给合成线程处理,最终生成页面,并显示到浏览器上(painting,display)
    • 从浏览器的performance可以看出,浏览器一开始发送请求,到获取数据后,第一件事就是解析HTML城DOM树,期间遇到css不会去parse,接着才是parse解析css,然后将解析后的css和DOM树进行布局,生成render tree,再更新图层树,进行layout,最后直接painting(绘制),然后触发load事件。
HTML解析
css为什么放顶部不放在底部。
  • 浏览器在解析HTML的过程中,从上往下,遇到一个标签渲染一个。当顶部遇到link的时候,css不会阻塞Html的解析

  • 但是如果顶部存放link标签,当HTML解析成DOM树的时候,需要与css样式表结合生成render tree。

    此时浏览器的操作:解析hmtl=》触发DomcontentLoad =>解析样式表 =》重新计算样式 =》更新图层=》绘制

    放在顶部的link,Html最后呈现的时候需要依赖他

  • 而如果link标签放在底部,那么html解析城dom树并且生成render tree,paintine到页面上不需要依赖link。

    浏览器的操作是:解析HTMl => 重新计算样式 =》布局=》绘制=》复合图层=》渲染

    等Link的css请求回来后,浏览器需要继续 解析样式表 =》 解析HTMl => 重新计算样式 =》触发相关事件

    所以css放在底部,可能会导致重绘效果。

js会阻塞dom的解析,需要暂停dom解析去执行js,js可能会操作样式,所以还需要等待样式加载完成。
<body>
    <div class="div">123123</div>
    <script>
        let i = 0
        while(i < 1000000000){
            i++
        }
        console.log(i);
    </script>
     <div class="div">123123</div>
</body>

上面这段代码,浏览器的操作时:

解析hmtl=>解析样式表(Js执行需要等待css加载完毕)=>js执行=>重新计算样式=》布局=》绘制=》遇到下面的div=》又开始解析Html=> 重新计算样式=>布局=》绘制

js会阻塞html解析,也会阻塞渲染,并且,js要等待上面的css加载完毕,保证js里面可以操作样式。

   <div class="div">123123</div>
    <script src="./1.js">
     
    </script>
     <div>123123</div>

将js抽离到文件去,通过script加载。

资源请求回来之后,默认会进行link script的预加载,所以css和js脚本是并行加载的。虽然执行步骤是跟上面一样的,但是css和js文件时并行请求的。

在这里插入图片描述

总结
  • 解析前遇到link和scirpt,会进行并行加载css和js文件。

  • html会生成字节流->分词器->tokens->根据token生成节点->插入到DOM树种。

  • css放在顶部,dom渲染会依赖css。放在底部,dom初次渲染不依赖,但是css加载完毕会引起dom的重绘。css不阻塞html解析

  • 内嵌在html的js会阻塞html解析,并且会等待当前脚本之上的样式表解析完毕后才会执行js(保证js可以操作css),然后才会继续解析html。js依赖css的加载。

  • js一般放在底部,为的是操作完整的dom和不影响html的解析。

在这里插入图片描述

渲染流程

往大了看,浏览器就是帮助我们发送请求,然后将响应资源加载出来的软件。我们可以自己模拟一个客户端。模拟获取数据并且解析html为dom树,和css解析成styleSheet的实现。

首先通过http模块创建一个服务器
const Http = require("http");
const fs = require("fs");
const path = require("path");
const server = new Http.createServer();

server.on("request", (req, res) => {
  console.log(req.headers);
  fs.createReadStream("./1.html").pipe(res);
});

server.listen(3000);
//这是要相应的内容
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .div {
            color: red;
        }
    </style>

</head>

<body>
    <div class="div">123123</div>
    <script>
        let i = 0
        while (i < 100000) {
            i++
        }
        console.log(i);
    </script>
    <div>123123</div>
</body>

</html>

接着模拟客户端发送请求:我们知道http是基于tcp连接的。我们只要自己实现httpi请求头,并且创建tcp连接,将数据发送出去即可。

const net = require("net");
//创建一个HttpRequest类,用来发送请求
class HttpRequest {
  constructor(options = {}) {
    this.host = options.host;
    this.method = options.method || "GET";
    this.port = options.port;
    this.path = options.path;
    this.headers = options.headers;
  }

  send() {
    return new Promise((resolve, reject) => {
      // 构建http请求
      const rows = [];
      rows.push(`${this.method} ${this.path} HTTP/1.1`); //模拟浏览器的请求行

      // 处理请求体的heades
      Object.keys(this.headers).forEach((item) => {
        rows.push(`${item}: ${this.headers[item]}`);
      });

      // 处理请求头
      // GET /  HTTP/1.1
      // xxx:xx
      //
      //
      const data = rows.join("\r\n") + "\r\n\r\n"; //加上换行符
      console.log("data", data);
      // 通过tcp传输
      const socket = net.createConnection(
        {
          host: this.host,
          port: this.port,
        },
        () => {
          console.log("创建连接成功");
          // 创建连接成功之后,传输http数据
          socket.write(data);
        }
      );

      let responseData = [];
      // 也是一个可读流,tcp传输是分段的。监听服务器数据返回,返回的不只有文件内容,还有响应头
      socket.on("data", function (chunk) {
        responseData.push(chunk);
      });
      socket.on("end", () => {
        responseData = Buffer.concat(responseData);
        let [headers, body] = responseData.toString().split("220");
        resolve({
          headers,
          body,
        });
      });
    });
  }
}

接着我们发送请求

async function request() {
  const request = new HttpRequest({
    host: "127.0.0.1",
    method: "GET",
    port: 3000,
    path: "/",
    headers: {
      name: "lin",
      age: 12,
    },
  });

  // 发送请求,响应行,响应头,响应体
  let { headers, body } = await request.send();
  // 处理body,对html解析生成dom树,对css文本解析,生成styleSheets。
}

关键就是自己实现http请求头,并且通过net创建了tcp连接,将数据发送出去。然后通过可读流,获取返回数据,返回的数据不仅有文件内容,还有响应头,如下:

HTTP/1.1 200 OK
Date: Tue, 22 Feb 2022 14:50:37 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

220
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .div {
            color: red;
        }
    </style>

</head>

<body>
    <div class="div">123123</div>
    <script>
        let i = 0
        while (i < 100000) {
            i++
        }
        console.log(i);
    </script>
    <div>123123</div>
</body>

</html>
0

这就是具体的响应内容。我们通过分割获取到html的部分。

接着模拟浏览器解析html为dom树。使用htmlparser2,
const HtmlParser = require("htmlparser2");
 // 发送请求,响应行,响应头,响应体
  let { headers, body } = await request.send();

  // html解析城dom tree,就是做词法分析最后生成dom tree
  // 解析后需要生城tree,典型的栈型结构
  let stack = [{ type: "document", children: [] }];
  // 浏览器根据响应内容来解析文件
  const parser = new HtmlParser.Parser({
    // document html header body
    // 遇到一个tag,获取他的tagName和属性
    onopentag(name, attributes) {
      let parent = stack[stack.length - 1];
      //
      let element = {
        tagName: name,
        attributes,
        children: [],
        parent,
      };
      parent.children.push(element);
      stack.push(element); // 当前的tag可能也有儿子
    },
    // 获取tag的内容
    ontext(text) {
      let parent = stack[stack.length - 1]; // 因为上面的tag已经作为一个元素Push进stack了,这里直接获取,将text放入到children就行
      let textNode = {
        type: "text",
        text,
      };
      parent.children.push(textNode);
    },
    // 关闭tag,遇到闭合的,就将其从栈中取出,到最后的html退出,stack此时剩一个document,他是一颗树,通过children跟parent将整个Html变成dom树。
    onclosetag(name) {
      //只考虑内联css
      if(name === 'style'){
        let parent = stack[stack.length - 1];
        const cssText = parent.children[0].text
        parserCss(cssText)
        console.log('cssText',cssText);
      }

      // 遇到闭合,
      stack.pop();
      if (stack.length === 1) {
        //console.dir(stack, { depth: null });
      }
    },
  });
  parser.write(body);

}

主要是通过一个栈结构,使用HtmlParser.parser,在遇到html标签的时候,将其压入栈中,然后匹配到内容的时候,将其存入对应元素的children,再到闭合标签的时候,将其推出栈。

比如document和header标签,遇到document标签,压入栈中,而下一个标签就是header,他会将header标签作为document的chidlren,并且将header标签压入栈中。然后遇到header标签的闭合标签,就将header标签推出栈,此时栈里只有一个document标签。他通过children连接到了header标签,以此类推,构建城整颗dom树。

等所有dom标签被解析后,最后栈里剩的就是一个document元素,他没有闭合标签。打印的结果应该是:

[
  <ref *4> {
    type: 'document',
    children: [
      { type: 'text', text: '\r\n' },
      { type: 'text', text: '\r\n' },
      <ref *2> {
        tagName: 'html',
        attributes: { lang: 'en' },
        children: [
          { type: 'text', text: '\r\n\r\n' },
          <ref *1> {
            tagName: 'head',
            attributes: {},
            children: [
              { type: 'text', text: '\r\n    ' },
              {
                tagName: 'meta',
                attributes: { charset: 'UTF-8' },
                children: [],
                parent: [Circular *1]
              },
              { type: 'text', text: '\r\n    ' },
              {
                tagName: 'meta',
                attributes: { 'http-equiv': 'X-UA-Compatible', content: 'IE=edge' },
                children: [],
                parent: [Circular *1]
              },
              { type: 'text', text: '\r\n    ' },
              {
                tagName: 'meta',
                attributes: {
                  name: 'viewport',
                  content: 'width=device-width, initial-scale=1.0'
                },
                children: [],
                parent: [Circular *1]
              },
              { type: 'text', text: '\r\n    ' },
              {
                tagName: 'title',
                attributes: {},
                children: [ { type: 'text', text: 'Document' } ],
                parent: [Circular *1]
              },
              { type: 'text', text: '\r\n    ' },
              {
                tagName: 'style',
                attributes: {},
                children: [
                  {
                    type: 'text',
                    text: '\r\n' +
                      '        .div {\r\n' +
                      '            color: red;\r\n' +
                      '        }\r\n' +
                      '    '
                  }
                ],
                parent: [Circular *1]
              },
              { type: 'text', text: '\r\n\r\n' }
            ],
            parent: [Circular *2]
          },
          { type: 'text', text: '\r\n\r\n' },
          <ref *3> {
            tagName: 'body',
            attributes: {},
            children: [
              { type: 'text', text: '\r\n    ' },
              {
                tagName: 'div',
                attributes: { class: 'div' },
                children: [ { type: 'text', text: '123123' } ],
                parent: [Circular *3]
              },
              { type: 'text', text: '\r\n    ' },
              {
                tagName: 'script',
                attributes: {},
                children: [
                  {
                    type: 'text',
                    text: '\r\n' +
                      '        let i = 0\r\n' +
                      '        while (i < 100000) {\r\n' +
                      '            i++\r\n' +
                      '        }\r\n' +
                      '        console.log(i);\r\n' +
                      '    '
                  }
                ],
                parent: [Circular *3]
              },
              { type: 'text', text: '\r\n    ' },
              {
                tagName: 'div',
                attributes: {},
                children: [ { type: 'text', text: '123123' } ],
                parent: [Circular *3]
              },
              { type: 'text', text: '\r\n' }
            ],
            parent: [Circular *2]
          },
          { type: 'text', text: '\r\n\r\n' }
        ],
        parent: [Circular *4]
      }
    ]
  }
]

可以看到形成了一颗以document开始的树结构。

解析css

假设我们的css只有内联,那么在Html匹配到style标签的时候,就应该处理css了、

const parser = new HtmlParser.Parser({    // document html header body    // 遇到一个tag,获取他的tagName和属性    onopentag(name, attributes) {      //匹配标签    },    // 获取tag的内容    ontext(text) {      let parent = stack[stack.length - 1]; // 因为上面的tag已经作为一个元素Push进stack了,这里直接获取,将text放入到children就行      let textNode = {        type: "text",        text,      };      parent.children.push(textNode);    },    // 关闭tag,遇到闭合的,就将其从栈中取出,到最后的html退出,stack此时剩一个document,他是一颗树,通过children跟parent将整个Html变成dom树。    onclosetag(name) {      //只考虑内联css      if(name === 'style'){        let parent = stack[stack.length - 1];        const cssText = parent.children[0].text        parserCss(cssText)      }      // 遇到闭合,      stack.pop();      if (stack.length === 1) {        console.dir(stack, { depth: null });      }    },  });

在遇到闭合标签的时候,在ontext阶段已经在text内部存入style这个元素的children,直接取出放入parserCss即可。

const css = require("css");  //解析cssfunction parserCss(styleText){   const ast =  css.parse(styleText) //解析成styleSheet   console.log('ast', ast);}

然后通过css这个包进行解析。

结果应该是

 .div {     color: red;  }// 转为{  type: 'stylesheet',  stylesheet: {    source: undefined,    rules: [      {        type: 'rule',        selectors: [ '.div' ],        declarations: [          {            type: 'declaration',            property: 'color',            value: 'red',            position: Position {              start: { line: 3, column: 13 },              end: { line: 3, column: 23 },              source: undefined            }          }        ],        position: Position {          start: { line: 2, column: 9 },          end: { line: 4, column: 10 },          source: undefined        }      }    ],    parsingErrors: []  }}

可以看到css文本被转为了一个styleSheeel对象。

优化策略

Perfomance API

在这里插入图片描述

步骤:

清除上一次请求 =》重定向 =》fetchStart(真正开始请求)=》Appcache检查缓存 =》 DNS解析 => 建立TCP连接 => 发送请求request => Response响应请求 => 处理资源 => onLoad

期间有很多的值,比如resetStart记载了开始请求的时间,responseEnd记载了请求结束的时间。

// 打印performance.timing{connectEnd: 1645577680989connectStart: 1645577680989domComplete: 1645577681183domContentLoadedEventEnd: 1645577681165domContentLoadedEventStart: 1645577681165 //DomContentLoad事件开始触发domInteractive: 1645577681085domLoading: 1645577681013domainLookupEnd: 1645577680989domainLookupStart: 1645577680989fetchStart: 1645577680989loadEventEnd: 1645577681183loadEventStart: 1645577681183 // load事件开始触发navigationStart: 1645577680989redirectEnd: 0redirectStart: 0requestStart: 1645577680989  // 请求开始responseEnd: 1645577680990 responseStart: 1645577680989  // 请求结束secureConnectionStart: 0unloadEventEnd: 1645577681010unloadEventStart: 1645577681010[[Prototype]]: PerformanceTiming}

通过这些值可以做性能监控。

在这里插入图片描述

DCL表示DomContentLoad事件的触发,FP表示有像素画到页面上了,就触发。 FCP表示首次内容绘制,L表示onLoad事件的触发时间。LCP表示可见区域最大的内容绘制的时间(就是你页面上需要绘制最久的dom元素。)

在这里插入图片描述

上面这些值可以通过performance.timing里的值计算。浏览器默认帮助我们计算好了

 <!-- 需要等待所有的事件执行完毕才会计算 -->    <script>        setTimeout(() => {            let {                fetchStart, //开始访问                requestStart,                responseStart,                responseEnd,                domInteractive, //dom树构建完毕,可以交互的时间点                domContentLoadedEventEnd, //dom加载完毕 + domContentLoad触发结束                loadEventStart, // 所有资源加载完毕            } = performance.timing;            let TTFB = responseStart - requestStart //从请求到数据返回第一个字节的所消耗的时间            let TTI = domInteractive - fetchStart //从开始访问,到dom可以交互所消耗的时间            let DCL = domContentLoadedEventEnd - fetchStart // DOM从请求数据,到dom加载完毕            let L = loadEventStart - fetchStart // 所有资源加载完毕所用的时长            console.log('TTFB,', TTFB);            console.log('TTI,', TTI);            console.log('DCL,', DCL);            console.log('L,', L);        }, 3000);    </script>

FP, FCP可以通过performance.getEntriesByType(‘paint’)获取具体时间

 const paint = performance.getEntriesByType('paint') console.log('paint', paint);结果是[    {        "name": "first-paint", FP(只是花了像素而已)        "entryType": "paint",        "startTime": 20.700000000186265,        "duration": 0    },    {        "name": "first-contentful-paint", FCP(必须有内容)        "entryType": "paint",        "startTime": 20.700000000186265,        "duration": 0    }]

在这里插入图片描述

可以看到时间是差不多的。

FMP表示有意义的绘制,如

   <div elementtiming="meaningful">haa</div>

表示是有意义的div,然后

PerformanceObserver观察性能测量事件,监听新的性能条目。

const observer =  new PerformanceObserver((entryList, observer) => { console.log(entryList.getEntries()); observer.disconnect() //首屏监测完毕后直接结束。  }) observer.observe({ entryTypes: ['element'] })//打印结果[    {        "name": "text-paint",        "entryType": "element",        "startTime": 47.59999999962747,        "duration": 0,        "renderTime": 47.59999999962747,        "loadTime": 0,        "intersectionRect": {            "x": 8,            "y": 49.60000228881836,            "width": 28,            "height": 20.80000114440918,            "top": 49.60000228881836,            "right": 36,            "bottom": 70.40000343322754,            "left": 8        },        "identifier": "meaningful",        "naturalWidth": 0,        "naturalHeight": 0,        "id": "",        "url": ""    }]

可获取该元素的渲染时间,作为FMP的时间。

检测LCP,最大内容渲染时间

    // LCP        new PerformanceObserver((entryList, observer) => {            console.log('lcp', entryList.getEntries());            observer.disconnect()        }).observe({ entryTypes: ['largest-contentful-paint'] })

结果是

[    {        "name": "",        "entryType": "largest-contentful-paint",        "startTime": 40.7,        "duration": 0,        "size": 2545,        "renderTime": 40.7,        "loadTime": 0,        "id": "",        "url": ""    }]

通过这个api也可以获取到LCP的时间。

FID时间,首次输入延迟

 // FID         new PerformanceObserver((entryList, observer) => {            console.log('FID', entryList.getEntries());            observer.disconnect()        }).observe({ type: ['first-input'], buffered: true })//当点击input标签后,打印[    {        "name": "mousedown",        "entryType": "first-input",        "startTime": 1000.0999999996275,        "duration": 16,        "processingStart": 1001.0999999996275,        "processingEnd": 1001.0999999996275,        "cancelable": true    }]

这几个就是首屏渲染的关键时间点,最好能记住!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

coderlin_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值