Skip to content
On this page

1. 浏览器缓存机制

缓存行为可分为 强缓存 、协商缓存 、启发式缓存

  1. 强缓存: 不向服务器进行请求,直接读取缓存状态码为 200
  2. 协商缓存: 向服务器发送请求,服务器根据请求头中的相关字段来判断是否命中缓存,如果命中缓存,则返回 304 状态码,否则返 回 200 状态码
  3. 启发式缓存: 浏览器根据一些规则来判断是否命中缓存

启发式缓存

启发式缓存是依赖浏览器自身的规则来判断何时请求资源以及何时缓存资源,也就是说它不需要与服务器进行通信。

WARNING

因为不需要与服务器进行通信,所以它无法知道服务器的最新状态,所以有的时候可能会造成请求了最新资源,但是浏览器还是读取了缓存。

资源相应头中没有出现 Expirecache-control:max-age 字段且没有出现 no-store 的情况下并且有设置 Last-Modified,浏览器才会启用 启发式缓存算法。但是这个算法的缓存时间因浏览器而异,比如会将响应头中的 Date 字段减去 Last-Modified 字段的 10% 来计算缓存时间。


协商缓存

协商缓存发生在通信双方三次握手之后,依靠 Last-Modified/If-Modified-Since,Etag/If-None-Match等标识来确定是否命中协商缓存,如果命中则返回 304 状态码,否则返回 200 状态码。

其中 Etag 是对文件内容的一次 hash 运算,Last-Modified 则是文件的最后修改时间。

所以 Last-ModifiedEtag 更加高效但是 Etag 更加准确。

WARNING

如果请求头中 Last-ModifiedEtag 都有值,那么服务器会优先选择 Etag 来进行缓存判断。


启发式缓存和协商缓存区别

todo


强缓存

强缓存在 http1.0 中是靠 Expire 字段实现,但是由于服务器时间和客户端时间不一致的情况下所以在 http1.1 中引入了 Cache-Control 字段。

Cache-Control 可以组合成多种指令,指令间由 , 分隔

// eg:
Cache-Control: max-age:3600, s-maxage=3600, public
Cache-Control: no-cache

缓存位置

  • Service Worker: 运行在浏览器背后的线程、无法直接访问 Dom 但是可以作为 离线缓存消息推送网络代理等用途。协议必须是 https
  • Memory Cache : 缓存在内存中
  • Disk Cache : 缓存在硬盘中
  • Push Cache : http2.0 的主动推动

WARNING

http2.0 中的主动推送功能在 Chrome106 版本后被禁用

浏览器会根据资源的大小、访问频率以及其他因素来决定将资源存储在内存缓存还是磁盘缓存中。通常,对于较小、频繁访问的资源,浏览器更倾向于将其存储在内存缓存中,而对于较大、不经常访问的资源,浏览器可能选择存储在磁盘缓存中。


缓存完整过程

浏览器第一次加载资源,服务器返回 200,浏览器将资源文件从服务器上请求下载下来,并把 response header 及该请求的返回时间一并缓存;

下一次加载资源时,先比较当前时间和上一次返回 200 时的时间差,如果没有超过 Cache-control 设置的 max-age,则没有过期,命中强缓存,不发请求直接从本地缓存读取该文件(如果浏览器不支持 HTTP1.1,则用 expires 判断是否过期);如果时间过期,则向服务器发送 header 带有 If-None-MatchIf-Modified-Since 的请求

服务器收到请求后,优先根据 Etag 的值判断被请求的文件有没有做修改,Etag 值一致则没有修改,命中协商缓存,返回 304;如果不一致则有改动,直接返回新的资源文件带上新的 Etag 值并返回 200

如果服务器收到的请求没有 Etag 值,则将 If-Modified-Since 和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回 304;不一致则返回新的 Last-Modified 和文件并返回 200

2. 浏览器渲染机制

  1. 浏览器会根据 HTML内容从上到下解析创建 DOM Tree
  2. 构建 CSS 树,当解析遇到外部样式时会异步下载,并下载完后解析成 CSS Tree 需要注意的是,解析过程中遇到图片、视频等资源都会异步下载,但是遇到 script 且没有 defer/async 时则会停止解析 DOMscript 下载完并执行完后再继续解析
  3. 合并 DOM TreeCSS Tree 构建 Render Tree
  4. 布局、将 Render Tree 上的计算每一个 DOM 节点在屏幕上的位置
  5. 绘制,这一步包括很多
    • 构建图层树,将 Render Tree 上的节点根据 CSS 属性(zIndex,合成图层等)进行分层,并按照图层的顺序进行绘制
    • 分块,将图层分割成一个个指令块,并将按照顺序组成绘制列表提交到合成线程中。
    • 光栅化,合成现成中将图层分成图块,进去光栅化线程池中转换成位图
      • 注意因为用户大部分关注视口部分,所以靠近视口部分的图层会优先被光栅化
    • 显示,光栅化完毕后则提交给浏览器进程显示到屏幕上

3. 浏览器资源解析机制

完整流程

  • 从解析 html 开始 document.readystate = "loading"
  • 解析完 dom 后,开始触发 DomContentLoad 事件
  • 此时可能还有 样式、图片等资源未加载完,所以 document.readystate = "interactive",等它们都加载完并执行完 document.readystate = 'complete' 才会触发 load 事件

为什么 CSS 需要放在头部

  1. 外链的 CSS 文件放在哪个位置都不会影响 html 的解析,因为浏览器会异步加载 CSS 文件
  2. 因为浏览器可以并行解析 CSSDOM ,如果放在尾部 DOM 被解析完再去下载样式文件会重新计算样式引起回流和重绘引起页面闪烁

为什么 Script 需要放在尾部

script 标签放在头部会阻塞 DOM 的解析,因为浏览器遇到 script 标签会停止解析 DOM 直到脚本加载并执行完毕

async 和 defer 的区别

  • async 表示异步加载,加载完立即执行,并不按照顺序
  • defer 表示延迟加载,加载完不会立即执行,而是等待 DOM 解析完再执行在 DOMContentLoad 事件触发前执行。而且是按照顺序
  • async 的优先级比 defer

预加载

预加载主要依靠 link 标签上面的 relpreloadprefetch 属性,告知浏览器提前加载好这些资源

  • preload 表示当前页面必定需要的资源,浏览器会优先加载
  • prefetch 表示当前页面可能需要的资源,浏览器会在空闲时加载
  • preload、prefetch仅仅是加载资源,并不会执行

预连接

预链接也是依靠 link 标签上面的 relpreconnectdns-prefetch 属性,告知浏览器提前建立与服务器的连接

html
<link rel="preconnect" href="https://example.com"></link>
<link rel="dns-prefetch" href="https://example.com"></link>

两者的区别

  • preconnect 建立连接(dns/tsl 握手,三次握手/重定向等),dns-prefetch 只解析跨域域名(只到 dns 这步)

4. 浏览器安全

跨站脚本攻击(XSS)

XSS 可分为 存储型、反射型、DOM 型

  • 存储型: 将危险监本存储在数据库中,在其他用户读取内容时被执行
  • 反射型: 是因为恶意脚本是通过作为网络请求的参数出现在 url 中,经过服务器解析响应,拼接在 HTML 中传回给客户端,然后浏览器解析执行恶意脚本。
  • 文档型: 文档形的 XSS 攻击其实也是恶意脚本被作为请求 URL 的参数;浏览器解析后作为脚本执行,和反射形的区别在于:由前端 JS 取出 URL 中的恶意代码并执行

措施

  • 转义字符,如 < 转义为 &lt;
  • 设置Cookiehttp-only 属性
  • 响应头中加入 CSP(Content-Security-Policy) 策略, 只允许加载指定域的脚本及样式

跨站伪造(CSRF)

CSRF 攻击者可以伪造用户的请求,以用户的身份向网站发起恶意请求

措施

  • 服务端验证请求头中的 referOrigin字段
  • 服务端验证请求参数中的 csrf token 字段
  • Cookie 设置 SameSite 不为 none;新版浏览器默认为 Lax

5. 跨域

只要协议、域名、端口、ip 中有一个不同都算跨域。

跨域解决方案

  1. jsonp
  2. 跨域资源共享(CORS)在服务器中的响应头设置 Access-Control-Allow-origin:'*';Access-Control-Allow-method:'*'
  3. postMessage
  4. nginx 反向代理
  5. nodejs 中间件代理

简单请求和非简单请求

  • 简单请求的请求方法必须是 GET/POST/HEAD
  • 简单请求的请求头只能包含以下字段
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
      1. application/x-www-form-urlencoded
      2. multipart/form-data
      3. text/plain

除此之外都是非简单请求,像我们常见的往请求头里面加 token 或者 Content-Type: application/json 都是非简单请求

6. 浏览器存储

  • Cookie 大小 5kb
  • sessionStorage 会话级
  • localStorage 大小 5mb
  • indexDB

Cookie 的属性有 http-only/domain/SameSite/max-age/secure )等 其中要注意

  • domain:指定了哪些主机可以访问该 Cookie 的域名。如果设置为.google.com,则所有以 google.com 结尾的域名都可以访问该 Cookie。注意第一个字符必须为.
  • SameSite: 限制那些情况不带 Cookie,可以预防 CSRF 攻击
    1. None: 任何情况下都不带 Cookie
    2. Lax: 大多数情况下不带 Cookie,只有当点击链接或提交表单时才会带 Cookie(默认值)
    3. strict: 站点相同时才携带 Cookie

例如我们想要在跨域请求中带上 Cookie,需要满足以下条件:

Request 请求设置 withCredentialstrue 服务器设置首部字段 Access-Control-Allow-Credentialstrue 服务器的 Access-Control-Allow-Origin 设置为\* 或者对应的域名;

7. 重绘和重排

  • 重绘:当节点需要更新属性而不会影响布局的,比如 color,就称为重绘
  • 重排:当节点需要更新布局,就称为重排。常见的情况有:
    1. 添加或者删除可见的 DOM 元素
    2. 元素的位置发生变化
    3. 元素的尺寸发生变化(包括外边距、内边框、边框大小、宽高、padding 等)
    4. 内容发生变化,比如文本变化或者图片被另一个不同尺寸的图片所替代
    5. 页面一开始渲染的时候,或者 resize 窗口的时候
    6. 获取 clientWidth/clientHight/computedStyle/innerHight/offset 等。只要是获取 layout 信息的操作,都会触发重排

如何减少重绘和重排

  • 避免频繁操作样式,最好一次性重写 style 属性,或者将多次操作合并为一次
  • 可以现将频繁操作的 DOM 元素设置为 display: none,操作结束后再显示出来
  • 批量增加 DOM 可以使用 createDocumentFragment 方法在批量插入
  • 缓存布局信息
  • 将动画效果应用到position属性为absolutefixed的元素上
  • resize/scroll 等事件进行防抖节流处理
  • 利用 CSS3 属性开启硬件加速 transform/opacity/filter

8. 浏览器路由

Hash

通过监听 hashChange 可以获取到 hash 值的变化,从而实现路由的切换

History

通过 popState 事件可监听到路由变化,需要注意的是 hashChange 事件也会触发 popState

9. 浏览器事件循环

js
// 0 1 2 3 4 5

0,1,2,3,4,5
[
  then(0)
  then(1)
  then(2)
  then(()=>4)
  then(3)
  then(4)
  then(5)
]

Promise.resolve()
  .then(() => {
    console.log(0);
    return Promise.resolve(4);// 可以改写成 Promise.resolve(4).then(()=>{return 4})
  })
  .then((res) => {
    console.log(res);
  });

Promise.resolve()
  .then(() => {
    console.log(1);
  })
  .then(() => {
    console.log(2);
  })
  .then(() => {
    console.log(3);
  })
  .then(() => {
    console.log(5);
  });