浏览器渲染原理 1

Posted lin-fighting

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浏览器渲染原理 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: 浏览器渲染原理

初探浏览器渲染原理

页面渲染原理

浏览器渲染原理及流程

浏览器渲染页面的原理及流程

iOS 图片渲染的原理1