浏览器背后的运行机制

2023-11-05

浏览器背后的运行机制


从本章开始,我们的性能优化探险也正式进入到了“深水区”——浏览器端的性能优化。

平时我们几乎每天都在和浏览器打交道,在一些兼容任务比较繁重的团队里,苦逼的前端攻城师们甚至为了兼容各个浏览器而不断地去测试和调试,还要在脑子中记下各种遇到的 BUG 及解决方案。即便如此,我们好像并没有去主动地关注和了解下浏览器的工作原理。我想如果我们对此做一点了解,在项目过程中就可以有效地避免一些问题,并对页面性能做出相应的改进。

“知己知彼,百战不殆”,今天,我们就一起来揭开浏览器渲染过程的神秘面纱!

浏览器的“心”

浏览器的“心”,说的就是浏览器的内核。在研究浏览器微观的运行机制之前,我们首先要对浏览器内核有一个宏观的把握。

开篇我提到许多工程师因为业务需要,免不了需要去处理不同浏览器下代码渲染结果的差异性。这些差异性正是因为浏览器内核的不同而导致的——浏览器内核决定了浏览器解释网页语法的方式。
浏览器内核可以分成两部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎。早期渲染引擎和 JS 引擎并没有十分明确的区分,但随着 JS 引擎越来越独立,内核也成了渲染引擎的代称(下文我们将沿用这种叫法)。渲染引擎又包括了 HTML 解释器、CSS 解释器、布局、网络、存储、图形、音视频、图片解码器等等零部件。

https://user-gold-cdn.xitu.io/2018/9/27/16619bf6505a94db?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

目前市面上常见的浏览器内核可以分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。

这里面大家最耳熟能详的可能就是 Webkit 内核了。很多同学可能会听说过 Chrome 的内核就是 Webkit,殊不知 Chrome 内核早已迭代为了 Blink。但是换汤不换药,Blink 其实也是基于 Webkit 衍生而来的一个分支,因此,Webkit 内核仍然是当下浏览器世界真正的霸主。

下面我们就以 Webkit 为例,对现代浏览器的渲染过程进行一个深度的剖析。

开启浏览器渲染“黑盒”

什么是渲染过程?简单来说,渲染引擎根据 HTML 文件描述构建相应的数学模型,调用浏览器各个零部件,从而将网页资源代码转换为图像结果,这个过程就是渲染过程(如下图)。

https://user-gold-cdn.xitu.io/2018/9/27/16618c7f0cb0768a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

从这个流程来看,浏览器呈现网页这个过程,宛如一个黑盒。在这个神秘的黑盒中,有许多功能模块,内核内部的实现正是这些功能模块相互配合协同工作进行的。其中我们最需要关注的,就是HTML 解释器、CSS 解释器、图层布局计算模块、视图绘制模块与JavaScript 引擎这几大模块:

  • HTML 解释器:将 HTML 文档经过词法分析输出 DOM 树。

  • CSS 解释器:解析 CSS 文档, 生成样式规则。

  • 图层布局计算模块:布局计算每个对象的精确位置和大小。

  • 视图绘制模块:进行具体节点的图像绘制,将像素渲染到屏幕上。

  • JavaScript 引擎:编译执行 Javascript 代码。

浏览器渲染过程解析

有了对零部件的了解打底,我们就可以一起来走一遍浏览器的渲染流程了。在浏览器里,每一个页面的首次渲染都经历了如下阶段(图中箭头不代表串行,有一些操作是并行进行的,下文会说明):

https://user-gold-cdn.xitu.io/2018/9/27/16618c829b879f35?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

  • 解析 HTML
    在这一步浏览器执行了所有的加载解析逻辑,在解析 HTML 的过程中发出了页面渲染所需的各种外部资源请求。

  • 计算样式
    浏览器将识别并加载所有的 CSS 样式信息与 DOM 树合并,最终生成页面 render 树(:after :before 这样的伪元素会在这个环节被构建到 DOM 树中)。

  • 计算图层布局
    页面中所有元素的相对位置信息,大小等信息均在这一步得到计算。

  • 绘制图层
    在这一步中浏览器会根据我们的 DOM 代码结果,把每一个页面图层转换为像素,并对所有的媒体文件进行解码。

  • 整合图层,得到页面
    最后一步浏览器会合并合各个图层,将数据由 CPU 输出给 GPU 最终绘制在屏幕上。(复杂的视图层会给这个阶段的 GPU 计算带来一些压力,在实际应用中为了优化动画性能,我们有时会手动区分不同的图层)。

几棵重要的“树”

上面的内容没有理解透彻?别着急,我们一起来捋一捋这个过程中的重点——树!

为了使渲染过程更明晰一些,我们需要给这些”树“们一个特写:

https://user-gold-cdn.xitu.io/2018/9/27/16619d637d220b20?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

  • DOM 树:解析 HTML 以创建的是 DOM 树(DOM tree ):渲染引擎开始解析 HTML 文档,转换树中的标签到 DOM 节点,它被称为“内容树”。

  • CSSOM 树:解析 CSS(包括外部 CSS 文件和样式元素)创建的是 CSSOM 树。CSSOM 的解析过程与 DOM 的解析过程是并行的。

  • 渲染树:CSSOM 与 DOM 结合,之后我们得到的就是渲染树(Render tree )。

  • 布局渲染树:从根节点递归调用,计算每一个元素的大小、位置等,给每个节点所应该出现在屏幕上的精确坐标,我们便得到了基于渲染树的布局渲染树(Layout of the render tree)。

  • 绘制渲染树: 遍历渲染树,每个节点将使用 UI 后端层来绘制。整个过程叫做绘制渲染树(Painting the render tree)。

基于这些“树”,我们再梳理一番:

渲染过程说白了,首先是基于 HTML 构建一个 DOM 树,这棵 DOM 树与 CSS 解释器解析出的 CSSOM 相结合,就有了布局渲染树。最后浏览器以布局渲染树为蓝本,去计算布局并绘制图像,我们页面的初次渲染就大功告成了。

之后每当一个新元素加入到这个 DOM 树当中,浏览器便会通过 CSS 引擎查遍 CSS 样式表,找到符合该元素的样式规则应用到这个元素上,然后再重新去绘制它。

有心的同学可能已经在思考了,查表是个花时间的活,我怎么让浏览器的查询工作又快又好地实现呢?OK,讲了这么多原理,我们终于引出了我们的第一个可转化为代码的优化点——CSS 样式表规则的优化!

不做无用功:基于渲染流程的 CSS 优化建议


在给出 CSS 选择器方面的优化建议之前,先告诉大家一个小知识:CSS 引擎查找样式表,对每条规则都按从右到左的顺序去匹配。 看如下规则:

#myList  li {}

这样的写法其实很常见。大家平时习惯了从左到右阅读的文字阅读方式,会本能地以为浏览器也是从左到右匹配 CSS 选择器的,因此会推测这个选择器并不会费多少力气:#myList 是一个 id 选择器,它对应的元素只有一个,查找起来应该很快。定位到了 myList 元素,等于是缩小了范围后再去查找它后代中的 li 元素,没毛病。

事实上,CSS 选择符是从右到左进行匹配的。我们这个看似“没毛病”的选择器,实际开销相当高:浏览器必须遍历页面上每个 li 元素,并且每次都要去确认这个 li 元素的父元素 id 是不是 myList,你说坑不坑!

说到坑,不知道大家还记不记得这个经典的通配符:

* {}

入门 CSS 的时候,不少同学拿通配符清除默认样式(我曾经也是通配符用户的一员)。但这个家伙很恐怖,它会匹配所有元素,所以浏览器必须去遍历每一个元素!大家低头看看自己页面里的元素个数,是不是心凉了——这得计算多少次呀!

这样一看,一个小小的 CSS 选择器,也有不少的门道!好的 CSS 选择器书写习惯,可以为我们带来非常可观的性能提升。根据上面的分析,我们至少可以总结出如下性能提升的方案:

  • 避免使用通配符,只对需要用到的元素进行选择。

  • 关注可以通过继承实现的属性,避免重复匹配重复定义。

  • 少用标签选择器。如果可以,用类选择器替代,举个?:

false:

#myList li{}

true:

.myList_li {}
  • 不要画蛇添足,id 和 class 选择器不应该被多余的标签选择器拖后腿。举个?:

false:

.myList#title

true:

#title
  • 减少嵌套。后代选择器的开销是最高的,因此我们应该尽量将选择器的深度降到最低(最高不要超过三层),尽可能使用类来关联每一个标签元素。

搞定了 CSS 选择器,万里长征才刚刚开始的第一步。但现在你已经理解了浏览器的工作过程,接下来的征程对你来说并不再是什么难题~

告别阻塞:CSS 与 JS 的加载顺序优化


说完了过程,我们来说一说特性。

HTML、CSS 和 JS,都具有阻塞渲染的特性。

HTML 阻塞,天经地义——没有 HTML,何来 DOM?没有 DOM,渲染和优化,都是空谈。

那么 CSS 和 JS 的阻塞又是怎么回事呢?

CSS 的阻塞

在刚刚的过程中,我们提到 DOM 和 CSSOM 合力才能构建渲染树。这一点会给性能造成严重影响:默认情况下,CSS 是阻塞的资源。浏览器在构建 CSSOM 的过程中,不会渲染任何已处理的内容。即便 DOM 已经解析完毕了,只要 CSSOM 不 OK,那么渲染这个事情就不 OK(这主要是为了避免没有 CSS 的 HTML 页面丑陋地“裸奔”在用户眼前)。

我们知道,只有当我们开始解析 HTML 后、解析到 link 标签或者 style 标签时,CSS 才登场,CSSOM 的构建才开始。很多时候,DOM 不得不等待 CSSOM。因此我们可以这样总结:

CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。

事实上,现在很多团队都已经做到了尽早(将 CSS 放在 head 标签里)和尽快(启用 CDN 实现静态资源加载速度的优化)。这个“把 CSS 往前放”的动作,对很多同学来说已经内化为一种编码习惯。那么现在我们还应该知道,这个“习惯”不是空穴来风,它是由 CSS 的特性决定的。

JS 的阻塞

不知道大家注意到没有,前面我们说过程的时候,花了很多笔墨去说 HTML、说 CSS。相比之下,JS 的出镜率也太低了点。
这当然不是因为 JS 不重要。而是因为,在首次渲染过程中,JS 并不是一个非登场不可的角色——没有 JS,CSSOM 和 DOM 照样可以组成渲染树,页面依然会呈现——即使它死气沉沉、毫无交互。

JS 的作用在于修改,它帮助我们修改网页的方方面面:内容、样式以及它如何响应用户交互。这“方方面面”的修改,本质上都是对 DOM 和 CSSDOM 进行修改。因此 JS 的执行会阻止 CSSOM,在我们不作显式声明的情况下,它也会阻塞 DOM。

我们通过一个?来理解一下这个机制:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>JS阻塞测试</title>
  <style>
    #container {
      background-color: yellow;
      width: 100px;
      height: 100px;
    }
  </style>
  <script>
    // 尝试获取container元素
    var container = document.getElementById("container")
    console.log('container', container)
  </script>
</head>
<body>
  <div id="container"></div>
  <script>
    // 尝试获取container元素
    var container = document.getElementById("container")
    console.log('container', container)
    // 输出container元素此刻的背景色
    console.log('container bgColor', getComputedStyle(container).backgroundColor)
  </script>
  <style>
    #container {
      background-color: blue;
    }
  </style>
</body>
</html>

三个 console 的结果分别为:

https://user-gold-cdn.xitu.io/2018/9/28/166203a2d62212c9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

注:本例仅使用了内联 JS 做测试。感兴趣的同学可以把这部分 JS 当做外部文件引入看看效果——它们的表现一致。

第一次尝试获取 id 为 container 的 DOM 失败,这说明 JS 执行时阻塞了 DOM,后续的 DOM 无法构建;第二次才成功,这说明脚本块只能找到在它前面构建好的元素。这两者结合起来,“阻塞 DOM”得到了验证。再看第三个 console,尝试获取 CSS 样式,获取到的是在 JS 代码执行前的背景色(yellow),而非后续设定的新样式(blue),说明 CSSOM 也被阻塞了。那么在阻塞的背后,到底发生了什么呢?

我们前面说过,JS 引擎是独立于渲染引擎存在的。我们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。JS 引擎对内联的 JS 代码会直接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。 因此与其说是 JS 把 CSS 和 HTML 阻塞了,不如说是 JS 引擎抢走了渲染引擎的控制权。

现在理解了阻塞的表现与原理,我们开始思考一个问题。浏览器之所以让 JS 阻塞其它的活动,是因为它不知道 JS 会做什么改变,担心如果不阻止后续的操作,会造成混乱。但是我们是写 JS 的人,我们知道 JS 会做什么改变。假如我们可以确认一个 JS 文件的执行时机并不一定非要是此时此刻,我们就可以通过对它使用 defer 和 async 来避免不必要的阻塞,这里我们就引出了外部 JS 的三种加载方式。

JS的三种加载方式
  • 正常模式:
<script src="index.js"></script>

这种情况下 JS 会阻塞浏览器,浏览器必须等待 index.js 加载和执行完毕才能去做其它事情。

  • async 模式:
<script async src="index.js"></script>

async 模式下,JS 不会阻塞浏览器做任何其它的事情。它的加载是异步的,当它加载结束,JS 脚本会立即执行。

  • defer 模式:
<script defer src="index.js"></script>

defer 模式下,JS 的加载是异步的,执行是被推迟的。等整个文档解析完成、DOMContentLoaded 事件即将被触发时,被标记了 defer 的 JS 文件才会开始依次执行。

从应用的角度来说,一般当我们的脚本与 DOM 元素和其它脚本之间的依赖关系不强时,我们会选用 async;当脚本依赖于 DOM 元素和其它脚本的执行结果时,我们会选用 defer。

通过审时度势地向 script 标签添加 async/defer,我们就可以告诉浏览器在等待脚本可用期间不阻止其它的工作,这样可以显著提升性能。

小结


我们知道,当 JS 登场时,往往意味着对 DOM 的操作。DOM 操作所导致的性能开销的“昂贵”,大家可能早就有所耳闻,雅虎军规里很重要的一条就是“尽量减少 DOM 访问”。

其它前端性能优化:

前端技术架构体系(没有链接的后续跟进):

其它相关

欢迎各位看官的批评和指正,共同学习和成长
希望该文章对您有帮助,你的 支持和鼓励会是我持续的动力

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

浏览器背后的运行机制 的相关文章

  • cuda异步并行执行

    异步函数使得主机端与设备端并行执行 控制在设备还没有完成前就被返回给主机线程 包括 kernel启动 以Async为后缀的内存拷贝函数 device到device内存拷贝函数 存储器初始化函数 比如cudaMemset cudaMemset
  • simulink中积分环节、惯性环节、比例环节

    第一步 第二步 第三步 注 适当修改参数即可变成所需环节 如下所示 修改为 或者为 抑或
  • hadoop搭建好,启动服务后,无法从web界面访问50070

    在hadoop完全分布式搭建好以后 从主节点启动正常 使用jps查看启动的进程 正常 在几个从节点上使用jps查看 显示正常 但从web上输入下面网址 http 主节点IP 50070 无法正常连接显示 试了若干网上查到的方法 是通过下面方
  • 怎样选择合适的循环体(do&while、while和for)

    我们都知道 循环体可以有五种 while do while for goto和递归 虽然理论上任何循环都可以用其他四种转换 但是因为goto在安全性以及在功能上能够被取代的特点 所以一般不会用到 而递归的特殊性和编写的困难性使递归的登场次数
  • 今天带你体验79毫秒启动一个SpringBoot项目

    大家好 我是雷小帅 今天来个项目实战 先抛一个问题 大家在自己电脑上启动一个 spring boot 项目需要花费多久 根据项目大小和机器环境 花费几秒到几十秒的人应该都有 最近 spring 官方推出了一项技术可以将项目的启动时间缩短到
  • 荒野行动服务器维护啥时好,荒野行动服务器真的极差

    说起 荒野行动 这个游戏 我想大家都不陌生 毕竟这个是在端游吃鸡出现不到三个月的时间 就由网易出品的一款吃鸡手游 可以说这个是第一款吃鸡手游 我玩了快半年的 荒野行动 了 先不说这个游戏咋样 咱们先说一说官网的态度 当真是不想让我们在继续玩
  • Vue2.0与Vue3.0的区别

    Vue2 0 Vue3 0 双向绑定 利用ES5的ApiObject defineProperty 对数据进行劫持 并结合发布订阅模式的方式实现 利用Es6的Proxy 对数据进行代理的方式实现 根节点 根节点只能是一个 根节点可以是多个
  • nginx之反向代理服务器

    本文摘抄自 深入理解Nginx 模块开发与架构解析 反向代理 reverse proxy 方式是指用代理服务器来接受Internet上的连接请求 然后将请求转发给内部网络中的上游服务器 并将从上游服务器上得到的结果返回给Internet上请
  • QGuiApplication底层鼠标处理(一)使用QSocketNotifier建立侦听连接

    QGuiApplication底层鼠标处理 一 使用QSocketNotifier建立侦听连接 读取外设信息 建立外设连接 init plugins QEvdevMousePlugin QEvdevMouseManager QEvdevMo
  • vmware 最近 win10 更新补丁后,需要更新后才能使用

    preface 最近准备换工作 在搭建自己的项目 一直未更新博文 等到 稳定后就把 项目流程 相关 发布出来 项目都是 搭建在 vmware 的 centos 中 尴尬的是 昨天 vmware 不能打开了 不能打开并且提示需要更新 vmwa
  • Unity实现UI在屏幕边缘跟随并指向视野外敌人

    最终效果 实现 心急的小伙伴可以直接跳到文章末尾查看最终代码 如果有问题再来看下思路 首先我们需要确定实现思路 我想到的方案是将玩家和敌人的世界坐标转换为UI坐标 然后求玩家和敌人坐标的线段与Canvas边界的交点即为箭头坐标 下面是求出交
  • 学习笔记TF043:TF.Learn 机器学习Estimator、DataFrame、监督器Monitors

    线性 逻辑回归 input fn 建立简单两个特征列数据 用特证列API建立特征列 特征列传入LinearClassifier建立逻辑回归分类器 fit evaluate 函数 get variable names 得到所有模型变量名称 可

随机推荐

  • Documentation/x86/entry_64.txt

    Chinese translated version of Documentation x86 entry 64 txt If you have any comment or update to the content please con
  • 【Linux操作系统】【综合实验三 用户帐号、文件系统与系统安全管理】【未整理】

    文章目录 一 实验目的 二 实验要求 三 实验内容 1 创建新用户帐号 并在用户主目录下放置用户文件 2 增添新的用户组 3 掌握chmod chgrp chown等命令的操作 熟悉其选择项功能 4 用户与用户组的修改 删除等操作及用户信息
  • 7.网络爬虫—正则表达式详讲

    7 网络爬虫 正则表达式详讲与实战 Python 正则表达式 re match 函数 re search方法 re match与re search的区别 re compile 函数 检索和替换 检索 替换 findall re findit
  • linux free命令详解

    一 作用 free命令可以显示当前系统未使用的和已使用的内存数目 还可以显示被内核使用的内存缓冲区 二 语法 free 选项 三 选项 默认情况下 即在没有选项的情况下 free 命令显示内存的使用信息 默认按照k b 的计数单位统计 to
  • vue+elementUI上传单张、多张图片/视频至oss

    1 上传单张图片 效果 创建oss js接口配置文件 import request from utils re js 封装的请求文件 import axios from axios export function policy1 retur
  • U盘启动盘安装Windows11 提示此电脑不符合安装windows11的最低系统要求

    1 Windows11安装失败 为了体验 Windows 11 系统 笔者也制作了 Win11 的 U盘 启动盘 来为自己的电脑安装 Windows 11 但是问题发生了 在使用 U盘 启动盘安装过程中提示笔者说 提示此电脑不符合安装win
  • 2023华为OD机试真题【同时出现的整数】

    题目内容 现有两个整数数组 需要你找出两个数组中同时出现的整数 并按照如下要求输出 1 有同时出现的整教时 先按照同时出现次数 整数在两人数组中都出现并目出现次数较少的那人 进行归类 然后按照出现次数从小到大依次按行输出 2 没有同时出现的
  • 攻防世界-level0

    攻防世界 level0 gdb peda run Starting program home giantbranch Desktop study level0 Hello World C Program received signal SI
  • 缓存与数据库的双写一致性

    背景 在高并发的业务场景下 系统的性能瓶颈往往是出现在数据库上 用户并发访问过大 压力都打到数据库上 所以一般都会用redis做缓存层 起到一个缓冲作用 让请求先访问到缓存层 而不是直接去访问数据库 减轻数据库压力 从而减少网络请求的延迟响
  • 你不知道的正则表达式理解

    正则表达式 Regular Expression 简称正则 一 什么是正则 在我们实际开发过程中经常会遇到 有查找符合某些复杂规则的字符串的需要 比如 我们要查找用户名 邮箱 手机号码等 这时候想匹配或者查找符合某些规则的字符串 就可以使用
  • ODS 、DW、 DM、CDC等名词解释

    Q ODS层 DW层 DM层是什么意思 A ODS层 DW层和DM层是数据仓库中的三个重要组成部分 它们分别代表了操作数据存储层 数据仓库层和数据集市层 ODS层 Operational Data Store 操作数据存储层 ODS层是数据
  • 金融ARQC、ARPC验证生成规则

    从2012年从事金融行业的IT开发和实施工作以来 接触最多的就是IC卡片的ARQC等安全验证 只从发行IC卡以来 行业里面安全验证就是使用ARQC来验证交易的安全性 最近在项目中实施改造的时候因为前段读卡上送过来的ARQC到我系统 我系统去
  • kafka对单分区重设偏移量

    一 整个kafka设置偏移量 对kafka整个集群设置偏移量大家使用较多 适合测试环境 丢弃整个消息队列中的数据 kafka consumer groups sh bootstrap server localhost 9092 group
  • arduino笔记28:使用TM1637四位数码管显示模块

    TM1637模块有四个引脚 相比于使用四位数码管的10个引脚 使用TM1637模块可以大大节省引脚数量 四个引脚的意义如下 GND 电源负级 VCC 电源正极 5V DIO 数据IO模块 可以接任意的数字引脚 CLK 时钟引脚 可以接任意的
  • yolo deepsort_基于YOLOv5和DeepSort的目标跟踪

    软硬件环境 windows 10 64bit pytorch yolov5 deepsort YOLOv5 前文 YOLOv5目标检测 和 YOLOv5模型训练 已经介绍过了YOLOv5相关的内容 在目标检测中效果不错 DeepSort S
  • 这可能是介绍Android UvcCamera最详细的文章了

    设备外接usb摄像头 进行基本的预览 拍照 录像 相信有些同学在工作中有遇到类似的需求 uvc camera 不管你之前有没用过 有没遇到过 相信看完这篇文章 一定会带给你一些收获 这篇文章将从下面几点展开讲解 一 什么是UVC 二 UVC
  • MySQL:MySQL8用户与角色管理

    文章目录 MySQL用户管理 创建用户 查看用户权限 添加 删除权限 查询表权限 使用新创建的用户登陆 修改密码 删除用户 mysql用户管理表 caching sha2 password插件 validate password组件的安装和
  • CSS3 连续向下循环播放动画

    向下动画是在 animate css 的基础上进行修改的 效果展示
  • Java加密工具类EncryptUtils

    Java 提供了一些常见的加密算法 如 MD5 SHA AES DES 现将这些实现方法放进加密工具类 EncryptionUtils 使用了 String format 来确保每个字节都能够正确的被转化为成十六进制字符串 而且不会因为缺少
  • 浏览器背后的运行机制

    浏览器背后的运行机制 从本章开始 我们的性能优化探险也正式进入到了 深水区 浏览器端的性能优化 平时我们几乎每天都在和浏览器打交道 在一些兼容任务比较繁重的团队里 苦逼的前端攻城师们甚至为了兼容各个浏览器而不断地去测试和调试 还要在脑子中记