浏览器的缓存机制

一、前言 缓存可以说是性能优化中简单高效的一种优化方式了。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。 对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。 接下来的内容中我们将通过缓存位置、缓存策略以及实际场景应用缓存策略来探讨浏览器缓存机制。 如需获取思维导图或想阅读更多优质文章请猛戳GitHub博客 二、缓存位置 从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。 Service Worker Memory Cache Disk Cache Push Cache 1.Service Worker Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。 Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。 当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。 2.Memory Cache Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。 那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢? 这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。 当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存 内存缓存中有一块重要的缓存资源是preloader相关指令(例如<link rel="prefetch">)下载的资源。总所周知preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。 需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。 3.Disk Cache Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。 ...

December 17, 2024

浏览器渲染过程

渲染流程 首先要了解的概念: 渲染引擎:它是浏览器最核心的部分是 “Rendering Engine”,不过我们一般习惯将之称为 “浏览器内核” 渲染引擎主要包括的线程: 各个线程主要职责: GUI渲染线程:GUI 渲染线程负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。当界面需要重绘(Repaint)或由于某种操作引发回流(Reflow)时,该线程就会执行。 JavaScript引擎线程: JavaScript 引擎线程主要负责解析 JavaScript 脚本并运行相关代码。 JavaScript 引擎在一个Tab页(Renderer 进程)中无论什么时候都只有一个 JavaScript 线程在运行 JavaScript 程序。需要提起一点就是,GUI线程与JavaScript引擎线程是互斥的,这也是就是为什么JavaScript操作时间过长,会造成页面渲染不连贯,导致页面出现阻塞的原理。 事件触发线程:当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待 JavaScript 引擎的处理。 通常JavaScript引擎是单线程的,所以这些事件都会排队等待JS执行。 定时器触发器: 我们日常使用的setInterval 和 setTimeout 就在该线程中,原因可能就是:由于JS引擎是单线程的,如果处于阻塞线程状态就会影响记时的准确,所以需要通过单独的线程来记时并触发响应的事件这样子更为合理。 Http请求线程: 在 XMLHttpRequest 在连接后是通过浏览器新开一个线程请求,这个线程就Http请求线程,它 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到 JavaScript 引擎的处理队列中等待处理。 有了上述的概念,对接下我们讲渲染流水线会有所帮助 简略版的渲染机制 很久之前就把浏览器工作原理读完了,看了很多博客,文章,当时简简单单的梳理一些内容,如下👇 简略版渲染机制一般分为以下几个步骤 处理 HTML 并构建 DOM 树。 处理 CSS 构建 CSSOM 树。 将 DOM 与 CSSOM 合并成一个渲染树。 根据渲染树来布局,计算每个节点的位置。 调用 GPU 绘制,合成图层,显示在屏幕上。 接下来大概就是这么说: 在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM 树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢。 ...

December 17, 2024

性能优化 面试题

共 52 道 性能优化 面试题。答案默认折叠,便于先自行作答。 1. 响应式开发中,如何避免窗口大小监听导致的重排抖动? 难度:2 · 类型:QA 题目要点 响应式开发中 resize 导致抖动的本质原因是窗口变化会高频触发事件,从而反复触发布局计算。常见优化方式包括对 resize 事件进行节流或防抖以降低执行频率,避免在回调中混合 DOM 读写以减少强制同步布局,利用 requestAnimationFrame 控制更新时机,以及使用 ResizeObserver、媒体查询或容器查询等浏览器能力,让布局响应更多地交给浏览器完成,从而降低重排和重绘的成本。 参考答案 在响应式开发中,如果直接监听 resize 事件并在回调中执行布局计算或 DOM 操作,很容易引发频繁的 重排(reflow)和重绘(repaint)。原因在于浏览器在拖动窗口尺寸时会持续触发 resize 事件,如果每次触发都进行样式读取和 DOM 更新,就会造成布局计算不断被打断,从而出现明显的抖动或性能下降。 实际工程中通常会从 事件触发频率控制、布局计算方式优化、浏览器能力利用 三个层面进行处理。 首先是对 resize 事件进行频率控制。浏览器在拖拽窗口时可能每秒触发几十到上百次 resize,如果每次都重新计算布局,主线程压力会非常大。常见做法是通过 节流(throttle)或防抖(debounce) 将计算频率降低。例如在节流策略下,每隔一段时间才执行一次布局更新,从而避免在连续拖动过程中频繁触发布局计算。对于布局实时性要求较低的场景,防抖也比较适合,即在窗口停止变化后再执行计算。 其次是避免在 resize 回调中混合 DOM 读写操作。浏览器在读取布局信息(例如 offsetWidth、getBoundingClientRect)时,如果之前存在未提交的样式修改,会强制触发布局计算,这被称为 强制同步布局(Forced Reflow)。因此更好的方式是将读取和写入操作进行分离,例如先批量读取尺寸,再统一修改样式,或者借助 requestAnimationFrame 将 DOM 更新安排到浏览器下一帧执行,避免频繁打断渲染流程。 另外一个更现代的方案是尽量减少对 window.resize 的依赖,而是使用 元素级尺寸监听机制。浏览器已经提供了 ResizeObserver API,可以直接监听某个容器尺寸变化。当布局响应依赖的是容器宽度而不是窗口宽度时,这种方式更加精确,同时也减少无关 resize 触发带来的性能损耗。 在 CSS 层面也可以减少 JavaScript 的参与。例如使用 媒体查询(media query) 或 容器查询(container query) 来处理布局变化,让浏览器在样式计算阶段直接完成响应式适配。CSS 驱动的响应式布局通常比 JavaScript 监听 resize 更高效,因为浏览器可以对样式计算进行统一调度。 ...