1 缓存(cache)
前端缓存也可以直接看作是 HTTP 缓存 和 浏览器缓存 的结合,两者是相辅相成的关系。
HTTP 缓存是产生于客户端与服务器之间通信的一种缓存,利用这一缓存可以提升服务器资源的重复利用率,在有效的时间内不必每次都向服务器请求相同的资源,大大减少服务器的压力;而浏览器缓存则是浏览器提供的一种缓存机制,可以将服务器资源和网页访问产生的临时数据缓存到内存或本地,提升客户端的加载速度。
2 http 缓存
2.1 Expires
Expires 首部字段是 HTTP/1.0 中定义缓存的字段,其给出了缓存过期的绝对时间,即在此时间之后,响应资源过期,属于实体首部字段。
因为 Expires 设置的缓存过期时间是一个绝对时间,所以会受客户端时间的影响而变得不精准。
2.1 Cache-Control
Cache-Control 首部字段是 HTTP/1.1 中定义缓存的字段,其用于控制缓存的行为
常用的指令有:max-age、s-maxage、public/private、no-cache/no store 等。
public 指令表示该资源可以被任何节点缓存
private 指令表示该资源只提供给客户端缓存,代理服务器不会进行缓存。
no store 不进行任何缓存。
no-cache 在请求首部中被使用时,表示告知服务器不直接使用缓存,要求向源服务器发起请求,而当在响应首部中被返回时,表示客户端可以缓存资源,但每次使用缓存资源前都必须先向服务器确认其有效性,这对每次访问都需要确认身份的应用来说很有用。
s-maxage 只适用于公共缓存服务器
同时当设置了 private 指令后 s-maxage 指令将被忽略。
max-age 指令给出了缓存过期的相对时间,单位为秒数。当其与 Expires 同时出现时,max-age 的优先级更高。但往往为了做向下兼容,两者都会经常出现在响应首部中。
2.2 Last-Modified 与 If-Modified-Since
Last-Modified 首部字段顾名思义,代表资源的最后修改时间。当浏览器第一次接收到服务器返回资源的 Last-Modified 值后,其会把这个值存储起来,并再下次访问该资源时通过携带 If-Modified-Since 请求首部发送给服务器验证该资源有没有过期。
如果在 If-Modified-Since 字段指定的时间之后资源发生了更新,那么服务器会将更新的资源发送给浏览器(状态码200)并返回最新的 Last-Modified 值,浏览器收到资源后会更新缓存的 If-Modified-Since 的值。
如果在 If-Modified-Since 字段指定的时间之后资源都没有发生更新,那么服务器会返回状态码 304 Not Modified 的响应。
2.2 Etag 与 If-None-Match
Etag 首部字段用于代表资源的唯一性标识,服务器会按照指定的规则生成资源的标识。当资源发生变化时,Etag 的标识也会更新。同样的,当浏览器第一次接收到服务器返回资源的 Etag 值后,其会把这个值存储起来,并在下次访问该资源时通过携带 If-None-Match 请求首部发送给服务器验证该资源有没有过期。
Etag 生成方式:
第一种方式:使用文件大小和修改时间。
第二种方式:使用文件内容的 hash 值和内容长度。
使用 eTag 服务器能够更加精准的分析资源的改变,同时浏览器也便能更加精准的控制缓存。
2.3 启发式缓存
缓存新鲜度 = max(0,(date - last-modified)) * 10%
根据响应报头中 date 与 last-modified 值之差与 0 取最大值后取其值的百分之十作为缓存时间。
date: Thu, 02 Sep 2021 13:28:56 GMT
age: 10467792
cache-control: public
last-modified: Mon, 26 Apr 2021 09:56:06 GMT
5 浏览器硬性重新加载
硬性重新加载模式强调的是“硬性”,可以理解为我们常说的“强制刷新网页”,
使用硬性重新加载后所有资源的请求首部都被加上了 cache-control: no-cache 和 pragma: no-cache,两者的作用都表示告知(代理)服务器不直接使用缓存,要求向源服务器发起请求,而 pragma 则是为了兼容 HTTP/1.0。
硬性重新加载并没有清空缓存,而是禁用缓存
tips: 因为硬性重新加载并没有清空缓存,当异步资源在页面加载完后插入时,其加载时仍然优先读取缓存 (Ctrl + F5),如果使用清空缓存并硬性重新加载便不会出现这种现象。
tips:如果采用开发者工具 Network 面板勾选 Disable cache 选项方式,那么异步资源也不会读取缓存,原因是缓存被提前禁用了,这与硬性重新加载不同。
base64 图片:几乎永远都是 from memory cache,不管是首次加载还是清空缓存都不奏效,
Base64 格式的图片被塞进 memory cache 可以视作浏览器为节省渲染开销的“自保行为”。
6 缓存获取顺序
- 如果有 service work ,则先访问
- 浏览器会率先查找内存缓存,如果资源在内存中存在,那么直接从内存中加载
- 如果内存中没找到,接下去会去磁盘中查找,找到便从磁盘中获取
- 如果磁盘中也没有找到,那么就进行网络请求,并将请求后符合条件的资源存入内存和磁盘中
浏览器内存缓存生效的前提下,JS 资源的执行加载时间会影响其是否被放入内存缓存
7 Preload 与 Prefetch
preload 也被称为预加载,其用于 link 标签中,可以指明哪些资源是在页面加载完成后即刻需要的,浏览器会在主渲染机制介入前预先加载这些资源,并不阻塞页面的初步渲染。
prefetch 则表示预提取,告诉浏览器加载下一页面可能会用到的资源,浏览器会利用空闲状态进行下载并将资源存储到缓存中。
<link rel="preload" href="https://i.snssdk.com/slardar/sdk.js" as="script" />
<link rel="prefetch" href="https://i.snssdk.com/slardar/sdk.js" />
8 service worker
Service Worker 本质上是一种 JavaScript 脚本,其作为一个独立的线程,它可以使应用程序能够控制网络请求,缓存这些请求以提高性能,并提供对缓存内容的离线访问,是渐进式 Web 应用程序。
Service Worker 缓存是持久的,独立于浏览器缓存或网络状态。
不同浏览器对 Service Worker 兼容性不同,同时出于安全考虑,Service worker 只能在 https 及 localhost 下被使用。
9 memory cache
Memory Cache 翻译过来便是“内存缓存”,顾名思义,它是存储在浏览器内存中的。其优点为获取速度快、优先级高,从内存中获取资源耗时为 0 ms,而其缺点也显而易见,比如生命周期短,当网页关闭后内存就会释放,同时虽然内存非常高效,但它也受限制于计算机内存的大小,是有限的。
10 disk cache
Disk Cache 翻译过来是“磁盘缓存”的意思,它是存储在计算机硬盘中的一种缓存,它的优缺点与 Memory Cache 正好相反,比如优点是生命周期长,不触发删除操作则一直存在,而缺点则是获取资源的速度相对内存缓存较慢。
11 网站自动登录 token
token 信息在客户端的存储及传输是用户不必重复登录的关键。
服务端自动植入:响应头中 set-cookie
前端手动存储: APP 或小程序中 主动存储: localStorage.setItem('token')
浏览器原理
blog.poetries.top/browser-wor…
1. 进程
1 浏览器进程:负责页面展示&交互
2 网络进程:处理网络资源
3 GPU进程:硬件加速
4 渲染进程:渲染网页
5 插件进程:硬件加速,提高体验
进程的特点:
-
进程中的任意一线程执行出错,都会导致整个进程的崩溃。
-
- 线程之间共享进程中的数据。
-
3.进程之间的内容相互隔离。
-
当一个进程关闭之后,操作系统会回收进程所占用的内存。
2. 线程
1 GUI渲染线程:负责解析HTML、CSS合成CSSOM树、布局树、绘制、分层、栅格化、合成。(重绘、重排、合成)和主线程是冲突的,当它执行时,主线程会被挂起;当主线程执行时,有需要此线程执行任务,保存到一个队列中,等待主线程执行完在执行。
2 主(JS引擎)线程:处理js代码(解析、执行)。消息队列不为空,循环取任务执行。由于主线程和GUI线程互斥、所以当JS任务执行过长时,会阻塞页面的渲染,造成卡顿。
3 事件触发线程:在js代码在解析时,遇到事件时,比如鼠标监听,会将任务添加到事件触发线程中。等事件触发时,会将任务从事件触发线程中取出,放到消息队列的队尾等待执行。
4 定时器触发线程:用于存放setTimeout/setInterval任务,在解析遇到这些任务时,主线程将这些任务放到定时器触发线程中,并开始计数,时间了之后,将任务放到消息队列中等待执行。
5 HTTP请求线程:用于检测XMLHttpRequest请求,当请求状态改变时,将设置的回调函数添加到消息队列中等待执行。
6 I/O 线程
3 输入URL到页面展示的过程
3.1资源请求阶段
3.1.1 用户:输入内容
浏览器进程接收到地址栏的URL请求,便将该URL转发给网络进程
3.1.2 网络进程:提交URL请求
-
-
- 如果是HTTP协议,此时会发起一个HTTP请求,
-
-
-
-
- 构建请求、查找本地资源缓存、查找本地DNS缓存、DNS获取IP
- 等待TCP队列、建立TCP连接、发送HTTP请求
-
-
-
-
- 网络进程发起真正的URL请求
- 服务器收到URL请求,返回响应头
- 网络进程收到响应头数据,解析响应头数据,然后转发给浏览器进程
-
3.1.3 浏览器进程:准备渲染进程
-
-
- 浏览器进程收到数据,发起渲染进程
- 渲染进程收到消息后,直接和网络进程建立数据管道,并准备接收HTML数据
-
3.1.4 渲染进程:提交文档
-
-
- 渲染进程向浏览器进程确认提交,告诉浏览器进程:已经准备好接收和解析页面数据
- 浏览器进程收到确认提交后,便移除之前旧的文档,然后更新浏览器进程中的页面状态
-
首先是发起主页面的请求,这个发起请求方可能是渲染进程,也有可能是浏览器进程,发起的请求被送到网络进程中去执行。网络进程接收到返回的 HTML 数据之后,将其发送给渲染进程,渲染进程会解析 HTML 数据并构建 DOM。这里你需要特别注意下,请求 HTML 数据和构建 DOM 中间有一段空闲时间,这个空闲时间有可能成为页面渲染的瓶颈。
3.2 渲染阶段
浏览器进程:等待渲染进程提交新页面、
网络进程:在渲染进程执行网页渲染时,同步下载HTML/CSS/JavaScript和图片等资源)、
渲染进程:开始渲染网页、
3.2.1 DOM:渲染进程将HTML内容转换为能够读懂的DOM树结构
3.2.2 CSSOM:渲染引擎将CSS样式表转化为浏览器可以理解的styleSheets,并计算出DOM节点的样式。
3.2.3 Layout:渲染引擎创建布局树,并计算元素的布局信息。
3.2.4 Layer:渲染引擎对布局树进行分层,并生成分层树
3.2.5 Paint:渲染引擎为每个图层生成绘制列表,并将其提交到合成线程
3.2.6 composition 合成线程:进行栅格化操作
-
-
- tiles:先将图层分成图块
- raster:然后在光栅化线程池中将图块转换成位图
- draw quad:最后发送绘制图块命令DrawQuad给浏览器进程
-
3.2.7 display:浏览器进程根据DrawQuad消息生成页面,并显示到显示器上
我们使用了CSS的transform来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率
3.3 DOM如何生成的
- 浏览器中的HTML解析器可以把HTML字符串转换成DOM结构
- HTML解析器边接收网络数据边解析HTML
解析DOM 详细:
-
- 通过分词器把HTML字符串转Token
- Token栈用来维护节点之间的父子关系,Token会一次压入栈中
- 如果是开始标签,把Token压入栈中并创建新的DOM节点并添加到父节点的children中
- 如果是文本 Token,则把文本节点添加到栈顶元素的children中,文本Token不需要入栈
- 如果是结束标签,此开始标签出栈
通过分词器产生的新 Token 就这样不停地压栈和出栈,整个解析过程就这样一直持续下去,直到分词器将所有字节流分词完成,最后生成DOM树。
3.4 资源加载
-
- CSS加载不会影响DOM解析
- CSS加载不会阻塞JavaScript加载,但是会阻塞JavaScript执行
- JavaScript依赖CSS加载,JavaScript会阻塞DOM解析
- defer:异步下载该脚本,顺序执行,等待HTML解析完毕后执行脚本
- async:异步下载该脚本,乱序执行,无论当前HTML是否解析完毕都会执行脚本
3.5 性能指标
-
- FP(First Paint 首次渲染) 表示浏览器从开始请求网站到屏幕渲染第一个像素点的时间
- FCP(First Contentful Paint首次内容渲染) 表示浏览器渲染出第一个内容的时间,这个内容可以是文本、图片或SVG元素等,不包括iframe和白色背景的canvas元素
- SI(Speed Index 速度指数) 表示网页内容的可见填充速度。衡量页面加载期间内容的视觉显示速度
- LCP(Largest Contentful Paint 最大内容绘制)标记了渲染出最大文本或图片的时间。测量感知加载速度的一个以用户为中心的重要指标
- TTI(Time to Interactive可交互时间) 指标测量页面从开始加载到主要子资源完成渲染,并能够快速、可靠地响应用户输入所需的时间
- TBI(Total Blocking Time总阻塞时间) 指标测量FCP与TTI之间的总时间,这期间,主线程呗阻塞的时间过长,无法做出输入响应
- FID(First Input Delay首次输入延迟) 测量加载响应度的一个以用户为中心的重要指标
- CLS(Cumulative Layout Shift 累计布局偏移)
- 优化
-
- FP&FCP
-
-
- 加快服务器相应速度
-
-
-
-
- 升级服务器配置
- 合理设置缓存
- 优化数据库索引
-
-
-
-
- 加大服务器带宽
- 服务器开启gzip压缩
- 开启服务器缓存
- 避免重定向操作
- 使用dns-prefetch进行DNS预解析
- 采用域名分片技术突破6个TCP连接限制或者采用HTTP/2
- 使用CDN减少网络跳转
- 压缩JS/CSS/图片等资源
- 减少HTTP请求,合并JS/CSS,合理内嵌JS和CSS
-
-
- SI
-
-
- 最小化主线程工作
- 减少JavaScript执行时间
- 确保文本在webfont加载期间保持可见
- 避免强制同步布局和布局抖动
-
-
- LCP
-
-
- 即时加载
- 优化关键渲染路径
-
-
- TTI
-
-
- 缩小JavaScript
- 预加载关键请求
-
3.6 代码的 V8 运行环境
-
-
- 解释和编译两种方式
- 解析流程: 源代码 => Parse => AST => Ignition 解释器 => byteCode 字节码 => TurboFan 编译器 => 机器码 => CPU 运行
-
-
-
-
- Parse 将JavaScript模块转成AST(抽象语法树)
- Ignition 解释器,将AST转成ByteCode (字节码是介于AST和机器码的中间代码)
- TurboFan 编译器,将字节码编译为CPU可以直接执行的机器码
-
-
词法分析:其作用是将一行行的源码拆解成一个个 token。所谓token,指的是语法上不可能再分的、最小的单个字符或字符串。
语法分析,其作用是将生成的 token 数据,根据语法规则转为 AST。如果源码符合语法规则,这一步就会顺利完成。但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。
如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。在执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符
4 浏览器垃圾回收-老生代回收花费时间长-可能会全停顿-
为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法。如下图所示:
使用增量标记算法,可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样当执行上述动画效果时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了。
5 使用 setTimeout 的注意事项
-
如果当前任务执行时间过久,会影延迟到期定时器任务的执行
-
如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒 (4毫秒延迟)
-
未激活的页面,setTimeout 执行最小间隔是 1000 毫秒 (浏览器节能)
-
延时执行时间有最大值 (大约 24.8 天,超出就立即执行)
-
使用 setTimeout 设置的回调函数中的 this 不符合直觉
6 微任务 与 宏任务
1 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
2 微任务的执行时长会影响到当前宏任务的时长。
3 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。
正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
async/await 就是 Promise 和生成器 generator 应用,往低层说就是微任务和协程应用
async 函数的理解:异步执行和隐式返回 Promise
await:父协程主线程任务执行完成后,拿到父协程的执行权,执行微任务 (Promise.then())
7 如何缩短白屏时长
- 通过内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了。
- 但并不是所有的场合都适合内联,那么还可以尽量减少文件大小,比如通过 webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。
- 还可以将一些不需要在解析 HTML 阶段使用的 JavaScript 标记上 sync 或者 defer。
- 对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件。
- 通过以上策略就能缩短白屏展示的时长了,不过在实际项目中,总是存在各种各样的情况,这些策略并不能随心所欲地去引用,所以还需要结合实际情况来调整最佳方案
8 页面分层合成流程
浏览器是怎么实现合成的:分层、分块和合成。
在 Chrome 的渲染流水线中,分层体现在生成布局树之后,渲染引擎会根据布局树的特点将其转换为层树(Layer Tree),层树是渲染流水线后续流程的基础结构。
层树中的每个节点都对应着一个图层,下一步的绘制阶段就依赖于层树中的节点。绘制阶段其实并不是真正地绘出图片,而是将绘制指令组合成一个列表,比如一个图层要设置的背景为黑色,并且还要在中间画一个圆形,那么绘制过程会生成|Paint BackGroundColor:Black | Paint Circle|这样的绘制指令列表,绘制过程就完成了。
有了绘制列表之后,就需要进入光栅化阶段了,光栅化就是按照绘制列表中的指令生成图片。每一个图层都对应一张图片,合成线程有了这些图片之后,会将这些图片合成为“一张”图片,并最终将生成的图片发送到后缓冲区。这就是一个大致的分层、合成流程。
需要重点关注的是,合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的。这就是为什么经常主线程卡住了,但是 CSS 动画依然能执行的原因。
9 分块
分层是从宏观上提升了渲染效率,分块则是从微观层面提升了渲染效率。
通常情况下,页面的内容都要比屏幕大得多,显示一个页面时,如果等待所有的图层都生成完毕,再进行合成的话,会产生一些不必要的开销,也会让合成图片的时间变得更久。
因此,合成线程会将每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块,这样就可以大大加速页面的显示速度。不过有时候, 即使只绘制那些优先级最高的图块,也要耗费不少的时间,因为涉及到一个很关键的因素——纹理上传,这是因为从计算机内存上传到 GPU 内存的操作会比较慢。
为了解决这个问题,Chrome 又采取了一个策略:在首次合成图块的时候使用一个低分辨率的图片。比如可以是正常分辨率的一半,分辨率减少一半,纹理就减少了四分之三。在首次显示页面内容的时候,将这个低分辨率的图片显示出来,然后合成器继续绘制正常比例的网页内容,当正常比例的网页内容绘制完成后,再替换掉当前显示的低分辨率内容。这种方式尽管会让用户在开始时看到的是低分辨率的内容,但是也比用户在开始时什么都看不到要好
10 css 动画比 js 动画快的原因
在写 Web 应用的时候,你可能经常需要对某个元素做几何形状变换、透明度变换或者一些缩放操作,如果使用 JavaScript 来写这些效果,会牵涉到整个渲染流水线,所以 JavaScript 的绘制效率会非常低下。
.box {
will-change: transform, opacity;
}
这段代码就是提前告诉渲染引擎 box 元素将要做几何变换和透明度变换操作,这时候渲染引擎会将该元素单独实现一帧,等这些变换发生时,渲染引擎会通过合成线程直接去处理变换,这些变换并没有涉及到主线程,这样就大大提升了渲染的效率。这也是 CSS 动画比 JavaScript 动画高效的原因。
所以,如果涉及到一些可以使用合成线程来处理 CSS 特效或者动画的情况,就尽量使用 will-change 来提前告诉渲染引擎,让它为该元素准备独立的层。但是凡事都有两面性,每当渲染引擎为一个元素准备一个独立层的时候,它占用的内存也会大大增加,因为从层树开始,后续每个阶段都会多一个层结构,这些都需要额外的内存,所以你需要恰当地使用 will-change
通常渲染引擎生成一帧图像有三种方式:重排、重绘和合成。其中重排和重绘操作都是在渲染进程的主线程上执行的,比较耗时;而合成操作是在渲染进程的合成线程上执行的,执行速度快,且不占用主线程。