内容整理来自极客时间李兵老师所讲专栏《浏览器工作原理与实践》
极客时间
-
栈空间和堆空间(js数据是如何存储的)
- 在声明变量之前需要先定义变量类型,我们把这种在使用前就需要确认变量数据类型的称为“静态语音”;而相反地,我们把运行过程中需要检查数据类型的语言称为动态语言(javascript就是个动态语言)
- 支持隐式类型转换的语言被称为“弱类型语言”(javascript),而不支持隐式类型转换的语言称为“强类型语言”
- 在javascript的执行过程中,主要有三种类型内存空间,分别是代码空间,栈空间,堆空间
function foo(){ var a = "极客时间" var b = a var c = {name:"极客时间"} var d = c } foo()
从上面来看原始类型的数值都是直接保存在“栈”中的,引用类型是存放在“堆”中。- 为什么要设计出如此模式?
是因为javascript引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换效率,进而又影响到整个程序的执行效率。
通常情况下,栈空间都不会设置太大,主要用来存放一些原始类型的小数据,而引用类型占用空间比较大,堆空间很大,能存放很多大的数据。 - 闭包的数据其实是存在了堆当中,而栈中存了个地址的引用。
-
垃圾回收:垃圾数据是如何自动回收?
- 栈垃圾回收
当函数执行结束,JS引擎通过向下移动”ESP”指针(记录调用栈当前执行状态的指针),来销毁该函数保存在栈中的执行上下文(变量环境,词法环境,this,outer) - 堆垃圾回收
2.1 代际假说
大部分对象存活时间都很短
不被销毁的对象,会活的更久
2.2 分类
V8引擎会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象
2.3 新生代
算法:Scavenge算法
原理:
2.3.1 把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域
2.3.2 新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作
2.3.3 先对对象区域中的垃圾做标记,标记完成之后,把这些存活的对象复制到空闲区域中
2.3.4 完成复制后,对象区域与空闲区域进行角色翻转,也激素hi原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域
2.3.5 经过两次垃圾回收依然还存活的对象,会被移动到老生区中
2.4 老生代
算法:标记-清除(Mark-Sweep)算法
原理:
2.4.1 标记:标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
2.4.2 清除:将垃圾数据进行清除
2.4.3 碎片:对一块内存多次执行标记-清除算法后,会产生大量不连续的内存碎片,而碎片过多会导致大对象无法分配到足够的连续内存
2.4.4 算法:标记-整理(Mark-Compact)算法
2.4.4.1 标记: 和标记-清除的标记过程一样,从一组根元素开始,递归遍历这组根元素,这个遍历过程中,能到达的元素标记为活动对象
2.4.4.2 整理:让所有存活的对象都向内存的一端移动
2.4.4.3 清除:清理掉端边界以外的内存
2.4.5 优化算法:增量-标记(Incaremental Marking)算法
2.4.5.1 为了降低老生代的垃圾回收而造成的卡顿
2.4.5.2 V8把一个完整的垃圾回收任务拆分为很多小的任务
2.4.5.3 让垃圾回收标记和js应用逻辑交替执行
- 栈垃圾回收
-
编译器和解释器:v8是如何执行一段js代码的?
-
V8如何执行一段js代码?
1.1 生成抽象语法树(AST)和可执行上下文1.1.1 AST(抽象语法树)
生成AST需要经过两个阶段:
第一个阶段是“分词”(tokenize),又称为词法分析:其作用是将一行行的源码拆解成一个个token,token指的是在语法上不可能再分的,最小的单个字符或者字符串。
第二个阶段是“解析”(parse),又称为语法分析:其作用是将上一步生成的token数据,根据语法规则转换为AST,如果源码符合语法规则,这一步就会顺利完成,但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。
1.1.2 生成可执行上下文(变量环境,词法环境,外部环境,this)1.2 生成字节码
解释器lgnition,它会根据AST生成字节码,并解释执行字节码,字节码就是介于AST和机器码之间的一种代码,但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。
同时Ignition也兼任了“生成字节码”和“解释字节码”两个作用,当Ignition执行字节码的过程中,如果发现有热点代码(比如同一段代码执行了多次),这个时候另一个编译器TurboFan就会把该热点的字节码编译为高效的机器码。这种模式字节码配合解释器和编译器称为“即时编译”(JIT)
-
JavaScript的性能优化
2.1 提升单次脚本的执行速度,避免js的长任务霸占主线程,这样可以使得页面快速响应交互
2.2 避免大的内联脚本,因为在解析HTML的过程中,解析和编译也会占用主线程
2.3 减少JavaScript文件的容量,因为更小的文件会提升下载速度,并且占用更低的内存
-
-
消息队列和事件循环:页面是怎么“活”起来的?
- 为什么需要消息队列和事件循环?
在每一个渲染进程都会有一个主线程,并且主线程非常繁忙,既要处理DOM,又要计算样式,还要处理布局,同时还需要处理JavaScript任务以及各种输入事件,要让这么多不同类型的任务在主线程中有条不紊地执行,所以需要该两者。 - 事件循环
想要在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制,这样就会一直来接收新的任务。
- 渲染进程中其他线程是如何发送消息给渲染主线程?
渲染主线程会频繁的接收来自于IO线程的一些任务,接收后,渲染线程就需要着手处理。比如:接收到资源加载完,需要进行DOM解析;比如:接收到鼠标点击后,需要进行javascript脚本来处理该点击事件。
- 消息队列
消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。
- 如何处理其他进程发来的消息?
渲染进程专门有一个IO线程用来接收其他进程传进来的消息(通过进程间的传输IPC,比如网络进程标识资源加载完成,比如浏览器进程表示鼠标点击),接收到消息后,会将这些消息组装成任务发送给渲染主线程,后续的步骤跟前面图一样处理逻辑。
- 页面使用单线程的缺点
页面线程中的所有执行的任务都来自于消息队列,消息队列是“先进先出”的属性,也就是说放入队列中的任务,需要等待前面的任务被执行完,才会被执行。
6.1 如何处理高优先级的任务
一个通用的设计是,利用javascript设计一套监听接口,当变化发生时,渲染引擎同步调用这些接口,这是一个典型的观察者模式。当DOM变化非常频繁,那么当前这个任务执行时间就会被拉长,从而导致“执行效率下降”。如果将DOM变化做成异步消息事件,添加到消息队列的尾部,那么又会影响到监控的实时性,因为添加到消息队列的过程中,前面就有很多任务在排队了。
这也就是说,如果DOM发生变化,采用同步通知的方式,会影响当前任务的执行效率,如果采用异步方式,又会影响到监控的实时性。因此“微任务”应用而生了。
通常我们把消息队列中的任务称为“宏任务”,每个宏任务中都包含了一个“微任务队列”
等当前宏任务的主要功能都直接完成之后,这个时候,渲染引擎并不会立即执行下一个宏任务,而是执行当前宏任务中的微任务,然后再执行下一个宏任务。
6.2 如何解决单个任务执行时长过久的问题
针对该情况,javascript可以使用回调功能来规避这种问题,也就是让要执行的javascript任务滞后执行。
- 为什么需要消息队列和事件循环?
-
WebAPI:setTimeout如何实现的?
浏览器的页面是通过消息队列和事件循环系统来驱动的。settimeout的函数会被加入到延迟消息队列中,等到执行完Task任务之后就会执行延迟队列中的任务。然后分析几种场景下面的setimeout的执行方式。
- 如果执行一个很耗时的任务,会影响延迟消息队列中任务的执行
- 存在嵌套带调用时候,系统会设置最短时间间隔为4s(超过5层)
- 未激活的页面,setTimeout最小时间间隔为1000ms
- 延时执行时间的最大值2147483647,溢出会导致定时器立即执行
- setTimeout设置回调函数this会是回调时候对应的this对象,可以使用箭头函数解决
-
WebAPI: xmlHttpRequest是怎么实现的?
-
async和await
-
生成器
生成器函数是一个带星号函数,而且是可以暂停执行和恢复执行的;
在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行;
外部函数可以通过 next 方法恢复函数的执行。 -
协程
2.1 为什么函数能暂停和恢复,这全部得归功于“协程“。协程是一种比线程更加轻量级的存在。可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。
2.2 比如当前执行的是A协程,要启动B协程,那么A协程就需要将主线程的控制权交给B协程,这就体现在A协程暂停执行,B协程恢复执行;同样,也可以从B协程中启动A协程。通常,如果从A协程启动B协程,我们就把A协程称为B协程的父协程。
-
Async
Async是一个通过异步执行并隐式返回Promise作为结果的函数。async function foo() { return 2 } console.log(foo()) // Promise {<resolved>: 2} 这里执行函数后 居然返回的是一个Promise对象,状态是reolved
-
Await
当执行await的时候,会默认创建一个Promise对象。比如:await 100 那么就会创建 new Promise(resolve,reject => { resolve(100) })
-
Async/await
本质上这个语法糖的基础技术使用了生成器和Promise结合,生成器是协程的实现,利用生成器能实现生成函数的暂停和恢复。
-
-
chrome开发者工具:利用网络面板做性能分析
-
工具介绍
-
NetWork(网络面板)
2.1 控制器
2.1.1 红色圆点的按钮,表示“开始 / 暂停抓包”,这个功能很常见,很容易理解。
2.1.2 “全局搜索”按钮,这个功能就非常重要了,可以在所有下载资源中搜索相关内容,还可以快速定位到某几个你想要的文件上。
2.1.3 Disable cache,即“禁止从 Cache 中加载资源”的功能,它在调试 Web 应用的时候非常有用,因为开启了 Cache 会影响到网络性能测试的结果。
2.1.4 Online 按钮,是“模拟 2G/3G”功能,它可以限制带宽,模拟弱网情况下页面的展现情况,然后你就可以根据实际展示情况来动态调整策略,以便让 Web 应用更加适用于这些弱网。2.2 过滤器
网络面板中的过滤器,主要就是起过滤功能。因为有时候一个页面有太多内容在详细列表区域中展示了,而你可能只想查看 JavaScript 文件或者 CSS 文件,这时候就可以通过过滤器模块来筛选你想要的文件类型。
2.3 抓图信息
抓图信息区域,可以用来分析用户等待页面加载时间内所看到的内容,分析用户实际的体验情况。比如,如果页面加载 1 秒多之后屏幕截图还是白屏状态,这时候就需要分析是网络还是代码的问题了。(勾选面板上的“Capture screenshots”即可启用屏幕截图。)
2.4 时间线
时间线,主要用来展示 HTTP、HTTPS、WebSocket 加载的状态和时间的一个关系,用于直观感受页面的加载过程。如果是多条竖线堆叠在一起,那说明这些资源被同时被加载。至于具体到每个文件的加载信息,还需要用到下面要讲的详细列表。
2.5 详细列表
这个区域是最重要的,它详细记录了每个资源从发起请求到完成请求这中间所有过程的状态,以及最终请求完成的数据信息。通过该列表,你就能很容易地去诊断一些网络问题。
2.6 下载信息概要
DOMContentLoaded,这个事件发生后,说明页面已经构建好 DOM 了,这意味着构建 DOM 所需要的 HTML 文件、JavaScript 文件、CSS 文件都已经下载完成了。
Load,说明浏览器已经加载了所有的资源(图像、样式表等)。 -
单个资源的时间线
3.1 发起http请求
3.2 浏览器查找缓存(若缓存没命中)
3.3 发起DNS请求获取IP地址
3.4 利用IP地址和服务器建立TCP连接
3.5 等待服务器响应
3.6 若服务器响应头包含了重定向信息,则整个流程重新走一遍。
3.7 Queueing(排队)-》Resource Scheduling阶段
当浏览器发起一个请求,会有很多原因导致该请求不能被立即执行,而是需要排队等待,排队的原因有很多:
3.7.1 页面中的资源是有优先级的,比如 CSS、HTML、JavaScript 等都是页面中的核心文件,所以优先级最高;而图片、视频、音频这类资源就不是核心资源,优先级就比较低。通常当后者遇到前者时,就需要“让路”,进入待排队状态。
3.7.2 浏览器会为每个域名最多维护 6 个 TCP 连接,如果发起一个 HTTP 请求时,这 6 个 TCP 连接都处于忙碌状态,那么这个请求就会处于排队状态。
3.7.3 网络进程在为数据分配磁盘空间时,新的 HTTP 请求也需要短暂地等待磁盘分配结束。
3.8 Stalled(停滞)-》Connection Start阶段
排队完成后,就要进入发起连接状态,不过在发起连接之前,还有一些原因导致连接过程被推迟
3.9 Proxy Negotiation(代理协商阶段)-》Connection Start阶段
表示代理服务器连接协商所用的时间
3.10 DNS Lookup (dns查询)-》Connection Start阶段
Dns查询所用的时间
3.11 Initial connection/SSL 阶段 -》Connection Start阶段
也就是和服务器建立连接的阶段,这包括了建立 TCP 连接所花费的时间;不过如果你使用了 HTTPS 协议,那么还需要一个额外的 SSL 握手时间,这个过程主要是用来协商一些加密信息的
3.12 Request Sent
网络进程会准备请求数据,并将其发送给网络, 只需要把浏览器缓冲区的数据发送出去就结束了,并不需要判断服务器是否接收到了
3.13 Waiting(TTFB)
接下来就是等待接收服务器第一个字节的数据,TTFB 是反映服务端响应速度的重要指标,对服务器来说,TTFB 时间越短,就说明服务器响应越快。
3.14 Content Download
接收到第一个字节之后,进入陆续接收完整数据的阶段, 这意味着从第一字节时间到接收到全部响应数据所用的时间。 -
优化线上耗时项
4.1 排队(Queuing)时间过久
4.1.1 大概率是由浏览器为每个域名最多维护6个连接导致的(参考域名分片技术)
4.1.2 把站点升级到HTTP2(无6个连接限制)
4.2 第一字节(TTFB)时间过久
4.2.1 服务器生成页面数据的时间过久
4.2.2 网络原因
4.2.3 发送请求头时带上了多余的用户信息
4.3 content download 时间过久
4.3.1 单个请求content download过久,有可能是字节数太多,这个时候减少文件大小,比如压缩,去掉源码中不必要的注释。
-
-
js是如何影响DOM树创建的
- 首先介绍了什么是DOM(表述渲染引擎内部数据结构,它将Web页面和JavaScript脚本连接起来,并过滤不安全内容)、DOM树如何生成(网络进程和渲染进程建立一个流式管道,HTML解析器直接解析,不需要等待text/html类型的接口 接受完毕再进行解析),第一步:通过分词器将字节流转换为Token;第二步:将Token解析为DOM节点;第三步:将DOM节点添加到DOM树中。
- JavaScript是如何影响DOM生成的?暂停html解析,下载解析执行完毕js之后再进行html解析(如果这期间使用到了cssDom,需要等待相应css过程)。预解析线程的优化(提前加载相应js css文件)
- 渲染引擎还有一个安全检查模块XSSAuditor用来检测词法安全的。
- Async和defer区别及用途
4.1 async: 脚本并行加载,加载完成之后立即执行,执行时机不确定,仍有可能阻塞HTML解析,执行时机在load事件派发之前。
4.1.1 只适用于外联脚本
4.1.2 如果有多个声明了async的脚本,其下载和执行也是异步的,不能确保彼此的先后顺序
4.1.3 async会在load事件之前执行,但并不能确保与DOMContentLoaded的执行先后顺序
4.2 defer: 脚本并行加载,等待HTML解析完成之后,按照加载顺序执行脚本,执行时机在DOMContentLoaded事件派发之前。
4.2.1 defer只适用于外联脚本,如果script标签没有指定src属性,只是内联脚本,不要使用defer,非常它会同步执行
4.2.2 如果有多个声明了defer的脚本,则会按顺序下载和执行
4.2.3 defer脚本会在DOMContentLoaded和load事件之前执行