
一个用户访问我们线上页面的时候明明代码已经发布了新版刷新好几次看到的却还是旧样式。这种问题我接过不止一次基本每次排查到最后都是缓存策略和资源加载时序配合出了问题。前端优化做到架构师这个层面单纯压体积、减请求已经不够了得把浏览器从拿到HTML到完成首屏渲染这条链路上的每一段都盘明白。这其中前置资源和缓存是离用户最近、也最容易出性价比高的优化点。这一节的内容就是围绕这两件事展开前置资源解决资源何时该开始加载、何时该执行的问题缓存解决资源能不能少加载、能不能直接用旧的的问题。两者配合好了页面首屏速度、弱网体验、发布稳定性都会有很直观的提升。适合已经具备一定前端基础、开始从整体视角思考性能瓶颈的开发者阅读。1. 先看整条链路从HTML到首屏时间都花在哪了在讨论前置资源和缓存之前有必要先把浏览器加载一个页面的基本路径过一遍。做前端优化最忌讳的就是脱离链路谈技巧很多方案单独拎出来都有道理放进整条链路里却互相打架。1.1 浏览器拿到HTML之后发生了什么当用户在地址栏输入网址并回车浏览器首先发起HTML请求。拿到HTML文档之后开始解析HTML标签遇到link、script、img这类带src或href的标签会按标签出现的顺序、资源的优先级去发起请求。渲染进程解析到CSS时会阻塞渲染除非是异步加载方式解析到普通script脚本时会阻塞解析器。这个过程叫关键渲染路径。整个首屏耗时里真正被用户感知的时间是从导航开始到首次内容绘制的时间。这段时间内浏览器干的活大致分成几块DNS解析、建立连接TCP握手、TLS协商、发送请求、等待响应、解析HTML、构建DOM和CSSOM、执行JavaScript、完成首次布局和绘制。很多优化方案的核心就是把这几块时间里可以提前做的提前做掉把可以不用做的省掉。前置资源针对的就是提前做——比解析器更早地告诉浏览器你接下来会用到哪些资源先帮我加载着。缓存针对的就是省掉做——这些资源我上回已经拿过了版本没变直接用本地或边缘节点的副本就行。1.2 关键资源数量对首屏的影响有多大我做过一个统计实验拿一个中等规模的活动页做基准测试。原始页面有23个请求首屏时间约2.8秒模拟4G网络。把其中8个非关键的图片和统计脚本加上loadinglazy和异步加载之后关键请求数降到9个首屏时间降到1.9秒。再配上preload提前加载首屏的主视觉和字体文件首屏能压到1.6秒左右。同样的优化在弱网环境下收益更明显3G网络下能快接近一倍。这个实验说明一个道理优化的时候不要把目光平行地放在所有资源上而是分清哪些资源在关键路径上、哪些不在。前置资源做的事情就是让关键路径上的资源加载得更早缓存做的事情是让关键路径上的资源能不加载就不加载。两个方向叠加首屏自然快。1.3 为什么很多团队做到了请求数很少但首屏还是慢还有一个经常被忽视的点请求数少不代表加载快。我见过一个管理后台页面压缩之后只有5个请求但首屏渲染依然要等很久。查了一下发现页面引用的一个核心JS文件没有被预先加载而是被一个动态import在某个组件渲染时才触发导致加载时机偏晚。还有一个场景主域名的DNS解析和TLS握手没有提前预热用户在弱网下打开页面的第一次连接建立就花了1秒多。这些问题的共性是链路上某个环节的等待时间没有被前置处理。分析到这一步前置资源和缓存这个主题的边界就清晰了——本质上是管理整个加载链路上的时序和复用而不是简单地记住几个标签属性。2. 前置资源的落地姿势preload、prefetch、preconnect、dns-prefetch前置资源在标准里的名字叫Resource Hints目的就是给浏览器提供提示信息让它可以提前做一些事情。注意这是一种提示不是指令。浏览器可以接受也可以忽略这和当前网络状况、设备内存、数据存储模式都有关系。2.1 preconnect和dns-prefetch把连接建立的时间提前很多页面慢不一定慢在资源体积而慢在建立连接。每次连接一个新域名至少要经历DNS查询走HTTPS的话还要再加上TLS握手这两个步骤在移动网络下尤其贵上百毫秒甚至一秒都算常见。dns-prefetch做的事情很简单让浏览器提前解析某个域名的DNS。preconnect更进一步它让浏览器提前完成DNS查询、TCP握手和TLS协商等真正请求时连接已经建立好了。!-- 提前解析第三方域名的DNS -- link reldns-prefetch hrefhttps://api.example.com !-- 提前建立到静态资源域名的连接 -- link relpreconnect hrefhttps://static.example.com crossorigin一个容易踩坑的点是preconnect的crossorigin属性。如果页面要请求的是跨域资源且不携带凭据比如CDN上的字体、图片preconnect需要加上crossorigin属性否则建立的连接和后续实际请求的连接不匹配等于白连。这个属性还影响连接池的复用方式很多人忽略这一点导致preconnect没起到预热效果。2.2 preload提前加载当前页面一定会用到的资源preload的语义是这个资源你马上面临使用时就会用到请你以合适的优先级先加载着。它最典型的应用场景是字体文件、首屏主视觉图片、以及被CSS或JavaScript间接引用的资源。以下几个场景是我实际项目中验证过的高收益用法!-- 提前加载字体文件避免FOIT闪白 -- link relpreload href/fonts/Inter.woff2 asfont typefont/woff2 crossorigin !-- 提前加载首屏背景大图 -- link relpreload href/images/hero.png asimage !-- 提前加载后一个路由的核心脚本 -- link relpreload href/js/detail-page.js asscript字体文件这里有个细节link relpreload asfont必须带crossorigin属性即使字体文件是同域的也一样。因为浏览器加载字体用的是匿名模式CORS请求如果不写crossoriginpreload会以cors模式请求而后续字体实际加载时以same-origin模式请求导致字体被加载两次反而拖慢速度。这是我在生产环境实测过的坑。as属性同样不能漏。它告诉浏览器资源的类型决定请求的优先级和Accept请求头。如果写了asscript但实际是样式文件优先级判断就会出错。2.3 prefetch和preload的边界优先级与浪费的权衡很多人把preload和prefetch混为一谈这两个确实容易混但语义差异很大。preload是当前页面马上要用prefetch是用户接下来可能去的地方会用到。!-- 预取下一个页面会用到的资源优先级很低 -- link relprefetch href/js/next-page.jsprefetch的资源会在浏览器空闲时加载优先级极低对当前页面的性能影响很小。但它有一个代价会消耗用户的带宽和流量。在移动端如果无限制地prefetch大量资源可能造成流量浪费甚至让用户产生不满。所以prefetch要克制一般只用于点击率最高的那个下一个页面比如列表页预取详情页的核心JS。preload和prefetch还有一个区别值得注意preload的资源如果没有在当前页面被使用浏览器会在控制台里提示was preloaded but not used。在开发环境这算个小警告但在生产环境它意味着你真的浪费了一批请求。因此preload只放那种确定会用、且尽快用的资源不能因为反正提前加载没坏处就滥用。2.4 优先级机制浏览器怎么决定先加载谁现代浏览器里每种资源类型都有默认的加载优先级。JavaScript脚本通常优先级很高CSS样式表也很高图片默认是中低优先级视屏内位置会提升。preload可以把资源的优先级提升到High甚至Highest。这个机制在实际开发中有一个很实用的玩法动态调整优先级。比如首屏图片默认优先级不高如果希望它尽快加载用preload提升优先级反过来如果希望某个脚本不要阻塞首屏可以用async或defer降低它的实际执行影响。下表是我整理的一个典型资源加载优先级对照资源类型默认优先级推荐的前置方式说明首屏背景图低视口内提升preload明确告诉浏览器尽早加载自定义字体中preload crossorigin避免FOIT当前页面核心JS高正常script标签即可preload通常不必要点击率高的下一路由JS低prefetch利用空闲时间加载第三方统计脚本低无需前置考虑异步加载跨域接口域名-preconnect缩短连接建立时间实践下来前置资源的核心原则就一句话把关键资源的加载时机从被解析到时提前到解析之前把非关键资源的加载时机延后到空闲时间。一前一后之间首屏的体验差别巨大。3. 缓存链路体系浏览器缓存、CDN缓存与源站缓存的协调缓存这个话题最容易出问题的不是某一层缓存没配好而是多层缓存之间的策略互相冲突。浏览器有浏览器缓存中间还有CDN缓存源站也可能有反向代理缓存和业务层的缓存。一条请求链路走过来每一层都在用自己的规则决定要不要返回缓存副本。3.1 HTTP缓存的两大门派强缓存和协商缓存HTTP缓存按是否能直接使用本地副本分为强缓存和协商缓存两种。强缓存的意思是浏览器在缓存有效期内直接使用本地副本不发起任何网络请求。控制强缓存的响应头主要是Cache-Control其中max-age表示缓存的有效秒数。比如Cache-Control: public, max-age31536000, immutable协商缓存的意思是资源可能过期了浏览器需要带着本地副本的标识去问服务器我这个还能用吗。服务器根据ETag或Last-Modified判断如果没变返回304 Not Modified响应体不返回浏览器继续使用本地副本如果变了返回200和新内容。从性能角度看强缓存最快——零网络请求。协商缓存次之——有一次请求但响应体很小。两者各有适用场景判断依据主要是这个资源会不会变、多久变一次。3.2 Cache-Control的指令每一个都要抠细节Cache-Control是整个HTTP缓存体系里最核心的响应头值得抠细一点。常见的指令和含义如下指令含义典型场景public任何节点包括CDN都可以缓存静态资源private只有浏览器可以缓存中间节点不能带用户信息的页面no-cache缓存前必须验证不能直接用需确保最新的动态资源no-store完全禁止缓存敏感数据max-age秒强缓存有效时间静态资源immutable文件内容永不变不用重新验证带hash的文件资源这里最容易混淆的是no-cache和no-store。no-cache不是不缓存而是使用缓存前必须先验证也就是说它走的是协商缓存路线。no-store才是真正的完全不缓存。这两个指令在面试里是高频考点在真实配置里也经常被写反。3.3 静态资源的长缓存策略与hash版本管理网上关于静态资源缓存的主流方案已经比较成熟但在实际落地中依然有很多团队没做到位。标准做法是文件名带上内容hash缓存时间设置得很长一年配合HTML文件使用no-cache。# 对带hash的静态资源 Cache-Control: public, max-age31536000, immutable # 对HTML文档 Cache-Control: no-cache这个组合的原理很直观文件内容一旦变更文件名hash就变了相当于一个新的URL浏览器自然会把旧缓存丢弃、请求新文件。HTML文件本身不缓存每次访问都回源校验保证能拿到最新的资源引用列表。这一套配合下来既最大化地利用了缓存又保证了发布后用户能拿到新版本。我见过不少团队卡在文件hash更新了但用户还是拿到旧文件这个问题上。排查下来通常是两个原因一是HTML文档也被设置了很长的max-age用户拿到的HTML本身就是旧的自然引用的还是旧资源二是部分代理服务器没有正确遵循no-cache把HTML也缓存了需要显式加Cache-Control: no-cache, no-store来强制。3.4 CDN缓存和回源策略的协调点引入CDN之后缓存体系多了一层。CDN节点会根据源站返回的Cache-Control决定缓存多久也会根据CDN平台的规则做覆盖。这里有三个容易踩坑的地方第一个坑是CDN缓存时间与源站缓存时间不一致。比如源站设置了max-age60CDN上却配了24小时就会导致源站内容更新后CDN还在分发旧版本。第二个坑是Set-Cookie响应头。如果资源响应里带了Set-Cookie很多CDN默认不缓存这类响应因为可能涉及用户隐私这会绕过CDN缓存回源增加源站压力。解决方式是把静态资源放在独立域名或独立路径确保响应里不带Set-Cookie。第三个坑是URL格式。CDN的缓存key默认是完整的URL如果同一个资源在页面里被用不同的查询参数引用比如?v123和?fromshareCDN会认为是两个不同的资源分别回源缓存。所以引用静态资源时尽量保证URL完全一致版本信息放到文件名hash里而不是查询参数里。我在一个项目中曾经把版本号放在查询参数里发布时把参数从?v1改成?v2原以为能正常更新结果某个CDN节点因为缓存配置不规范迟迟不更新导致用户大量反馈样式错乱。后来统一改成文件名hash方案把查询参数彻底移除问题才彻底解决。缓存体系的落地尽量把为什么会这样想清楚每一层缓存决定是否命中的依据是什么、谁能决定内容失效、失效之后从哪里拿最新内容。三层都想通了配置才不会打架。4. 缓存失效与更新发布之后用户为什么还看到旧版本缓存的另一个核心难题不是怎么缓存而是怎么失效。生产环境里线上大半的更新不生效问题都出在缓存失效策略上。写缓存的时候所有人都会但失效策略最见功力。4.1 带hash的静态资源更新机制是怎么工作的先理清一个关键问题带hash的静态资源发生变化时到底发生了什么。假设页面引用了app.a1b2c3.js这个文件内容不变的情况下浏览器也好、CDN也好都会命中缓存这是一件好事。当代码变更后打包工具生成了新文件app.d4e5f6.js同时HTML里引用的地址也变成了app.d4e5f6.js。此时用户打开页面HTML因为no-cache每次回源校验拿到的是最新版本引用了新的JS地址。这个新地址在CDN和浏览器里都是第一次见到自然触发回源拉取然后正常缓存。整个更新过程是平滑的。但这里面有一个被很多人忽略的隐患旧文件app.a1b2c3.js和它的缓存并不会因为新文件出现而主动消失。访问过旧页面的用户在相当长一段时间内会同时持有新旧两个缓存文件。如果旧文件恰好引用了某些已下线接口而这些接口在服务端已经不能用了就会导致部分用户那些还停留在旧页面的用户出现功能异常。这里有一个可行的做法在发布新版本时对CDN上旧的静态资源做主动刷新操作让旧文件的缓存也尽快清理。如果CDN支持批量刷新可以按目录刷新或者按文件列表精确刷新。源站服务器上保留旧文件一段时间避免极端情况下离线包或本地缓存回源时找不到文件。4.2 index.html为什么不能缓存太长时间HTML是最容易出现缓存问题的文件类型。很多团队把HTML当成普通静态资源直接设置了max-age86400甚至更长。这带来一个直接后果发布之后24小时内用户根本访问不到新版本页面。正确配置是HTML统一走no-cache让它每次都回源做协商校验。但这一步要注意区分HTML文档和纯静态资源如果整个站点都是静态化产物并且发布频率很低HTML可以设置一个尽量短的有效期比如max-age60这样既减少了回源压力又能保证最多一分钟后用户能拿到新版本。对于带有用户登录态、个性化信息的HTML必须用no-store或者private, no-cache防止代理服务器把它缓存在公共节点上导致其他用户拿到别人的页面片段。4.3 灰度发布环境下的缓存策略灰度发布是另一个容易和缓存冲突的场景。正常灰度流程是一部分用户访问新版本代码另一部分用户访问旧版本。但如果HTML被缓存了用户会被钉在某个版本上灰度比例完全失真。灰度发布时的缓存策略我建议把HTML的Cache-Control设置成no-cache, must-revalidate同时确认中间层比如CDN或反向代理不会忽略这个指令。如果担心回源压力大可以接受一定程度的访问在源站前加一层短时缓存比如1秒来削峰而不是在用户侧做长缓存否则灰度效果无从谈起。另外灰度发布时带有新功能开关的静态资源也要注意。如果app.js里包含了新旧两套逻辑用开关控制那缓存策略就相对简单。如果是新旧两套独立文件灰度期间两个版本的资源文件会同时存在需要确认两个文件在CDN上都可访问并且都不带Set-Cookie。4.4 缓存一致性问题的高频排查路径线上遇到用户看到旧版本的反馈时不要慌更不要第一时间怀疑浏览器按下面这条链路逐步排查确认你的HTML响应头里的Cache-Control是什么。F12打开Network面板点开页面文档的请求查看响应头。如果max-age不为0且没有no-cache那基本就是它导致的改配置就好。确认CDN上一级是否缓存了HTML。很多CDN产品会默认忽略源站的某些指令或者有自己的缓存规则。可以在CDN控制台看命中日志如果HTML的命中率异常高比如接近100%说明CDN缓存了它。确认静态资源URL是否真的变化了。用无痕窗口打开页面看Network里加载的JS文件名和发布平台上的文件名是否一致。不一致说明HTML引用链已经断了。确认用户侧是否处于代理网络中。部分网络代理会额外缓存资源这种情况下只能让用户切换网络测试来规避。避坑的关键在于不要让缓存导致看不到新版这个问题成为一个玄学把每一层的缓存策略都显式化、可观测化问题出现时就能快速定位到具体某一层。这里我通常会在源站统一封装一个响应头工具所有静态资源响应都走统一的配置中心避免每个业务线自己写一套缓存头。5. Service Worker缓存从HTTP缓存到应用缓存的升级HTTP缓存虽然好用但它有一个先天局限缓存策略由服务器决定浏览器只能被动遵循。而Service Worker把缓存控制权完全交给了应用层前端可以自己决定请求走缓存、走网络、还是走网络优先、缓存兜底的混合策略。5.1 Service Worker能做什么Service Worker是一个独立于页面运行在浏览器后台的JavaScript线程可以拦截页面的所有网络请求并且能访问Cache Storage API进行精细的缓存管理。这意味着它可以在HTTPS和localhost环境下实现完全自定义的缓存策略。第一层价值是离线能力。用户断网或者弱网时页面内容依然可以从Service Worker缓存的副本中恢复这在移动端的价值很大。第二层价值是缓存策略的精细化。HTTP缓存只能按URL来简单区分Service Worker可以按请求类型、网络状态、甚至用户行为来做不同策略。比如图片资源走缓存优先接口请求走网络优先、失败缓存兜底版本发版时可以主动清理旧缓存。第三层价值是性能优化。配合precache构建时预缓存关键资源页面二次打开时几乎可以瞬间加载因为核心资源在Service Worker安装时就已经缓存了。5.2 一个可落地的Service Worker缓存策略模板我实际项目里用过一套比较成熟的策略分享出来做个参考。核心思路是静态资源precache、动态接口network-first、图片cache-first。// sw.js const CACHE_NAME app-v1 // 静态资源预缓存列表 const PRECACHE_URLS [ /, /css/app.css, /js/app.js ] self.addEventListener(install, (event) { event.waitUntil( caches.open(CACHE_NAME).then((cache) { return cache.addAll(PRECACHE_URLS) }) ) self.skipWaiting() }) self.addEventListener(activate, (event) { event.waitUntil( caches.keys().then((keys) { return Promise.all( keys.filter((key) key ! CACHE_NAME) .map((key) caches.delete(key)) ) }) ) self.clients.claim() }) self.addEventListener(fetch, (event) { const { request } event // 忽略非GET请求 if (request.method ! GET) return // 导航请求网络优先失败时回退缓存 if (request.mode navigate) { event.respondWith( fetch(request) .then((response) { const copy response.clone() caches.open(CACHE_NAME).then((cache) cache.put(request, copy)) return response }) .catch(() caches.match(request)) ) return } // 静态资源缓存优先未命中再走网络并缓存 event.respondWith( caches.match(request).then((cached) { if (cached) return cached return fetch(request).then((response) { const copy response.clone() caches.open(CACHE_NAME).then((cache) cache.put(request, copy)) return response }) }) ) })这里有几个关键细节skipWaiting和clients.claim配合使用让新的Service Worker在激活后立刻生效而不是等到用户下次打开页面。版本号app-v1的作用是升级时整体淘汰旧缓存。每次发布时把版本号改掉activate里会把旧版缓存全部清除避免旧缓存越积越多。导航请求走网络优先而不是缓存优先是为了保证用户每次访问页面都能拿到最新HTML。这个思路和HTTP缓存里no-cache的目的一致。5.3 Service Worker使用中的几个真实坑Service Worker虽然强大但坑也不少。先说我遇到的第一个坑很多人以为Service Worker能用浏览器就会用它。其实Service Worker有拦截范围scope的限制假如脚本放在/js/sw.js默认只能控制/js/下的路径不设置Service-Worker-Allowed响应头的话很难控制全站。注册时需要确认scope是否符合预期。第二个坑是precache和构建流程的配合。手动维护PRECACHE_URLS列表极其容易过期生产环境必须用工具比如Workbox自动生成预缓存清单。Workbox可以把构建产物中的文件列表和hash自动注入到Service Worker脚本里每次发布自动更新避免预缓存清单里写了不存在的文件导致安装失败。第三个坑是调试体验。Service Worker更新不是即时的修改了sw.js之后默认情况下需要用户关闭所有同源页面再重新打开才能生效。开发时建议在DevTools的Application面板里勾上Update on reload同时配合skipWaiting和clients.claim能让调试顺畅很多。第四个坑是存储空间的占用。Service Worker缓存没有上限如果缓存策略写得激进缓存了海量图片和接口数据会占用大量用户磁盘空间。这需要在删缓存逻辑里定义上限或者按时间戳淘汰旧数据我通常在activate阶段做版本升级清理在fetch策略里再加一层LRU类似的淘汰逻辑保证缓存体积可控。5.4 Service Worker与HTTP缓存的配合边界引入Service Worker之后不等于HTTP缓存就不需要了。两者依然有分工。Service Worker负责应用层的策略编排HTTP缓存负责网络层的兜底。请求发出后浏览器会根据HTTP缓存的规则决定是否发起真实请求Service Worker在请求发出之前就已经拦截了。一个常见的误用是把Service Worker缓存设置得很长却在HTTP层设置了no-cache。这会导致每次请求都回源Service Worker的缓存形同虚设。反过来HTTP层设置了超长缓存但Service Worker走网络优先策略也会导致更新策略失效。我的经验是Service Worker和HTTP缓存的方向要一致。静态资源在HTTP层设置长缓存在Service Worker层也设置长缓存HTML在HTTP层设置no-cache在Service Worker层走网络优先。两层策略对齐才不会出现一个说要缓存、一个说不缓存的互相打架现象。以我实际项目的效果来看接入Service Worker之后移动端弱网环境的二次打开速度提升了40%左右页面即使断网也能展示出上一次的内容骨架。但代价是开发调试链路复杂度上升、缓存排障多了一个维度团队如果没有专门的精力维护建议先用好HTTP缓存再到渐进增强阶段去接Service Worker。6. 缓存治理的工程化实践可观测、可配置、可刷新前端优化进入架构层面后缓存方案一定要工程化。这里的工程化包含三个要素可观测——知道缓存的命中情况可配置——改缓存策略不需要发版可刷新——线上缓存出问题能立刻清理。6.1 给缓存加一层可观测性做缓存最怕的就是黑盒。用户报告页面异常你连是缓存导致还是代码问题都判断不出来那就很难推进了。给缓存加可观测性的几个手段第一在响应头里保留足够的缓存相关信息。CDN平台一般都会在响应头里返回X-Cache: HIT/MISS之类标记浏览器响应头里的Age字段表示资源已经存在缓存中的时间。这些信息在排障时非常有用。第二将核心资源的缓存命中率做成指标。可以通过CDN控制台或自建日志分析获得静态资源命中率数据。正常情况静态资源的缓存命中率应该达到90%以上如果持续走低大概率是URL变动太频繁或者缓存配置有问题。第三在前端运行时做缓存状态的上报。页面加载时读取performance.getEntriesByType(resource)可以获得每个资源是否命中了缓存、加载耗时多少把这些数据汇总上报可以用来分析真实用户场景下的缓存收益。6.2 用统一的缓存配置中心替代散落的响应头在一个多团队协作的前端组织里各个业务线各自写自己的缓存响应头配置是一件风险很高的事。比如有的团队忘了给HTML加no-cache有的团队给JS设置了过短的缓存时间这些问题单看都不大但组合之后就很难管理。更保险的做法是把缓存配置统一收敛到一个配置中心。静态资源服务通过一个统一的接入层来设置响应头按路径和文件类型分组配置。比如路径模式Cache-Control说明/static/*.jspublic, max-age31536000, immutable带hash的静态脚本/static/*.csspublic, max-age31536000, immutable带hash的样式文件/static/img/*public, max-age86400图片允许较短缓存/*.htmlno-cacheHTML回源校验/api/*no-store接口数据不缓存配置集中的好处是任何人修改缓存策略都走统一评审出现问题也只需要在一个地方排查。对于CDN那层也可以把配置中心作为唯一出口避免各业务线直接在CDN控制台上各改各的最终形成一笔糊涂账。6.3 线上缓存紧急清理的SOP无论方案设计得多完善总有需要手动介入的时刻。比如发布了一个错误版本需要通过缓存最快地让用户看到回滚版本或者源站文件被误删CDN还持有旧缓存用户还能正常访问但缓存回源即将失败。这种时候需要一套明确的SOP。第一步确认影响范围。从这个资源的访问日志或者CDN控制台查最近几小时该URL的命中量判断有多少用户在受影响范围内。第二步清理缓存。在CDN控制台对对应URL或目录做刷新操作。优先用精确URL刷新影响面最小只有URL不确定时才用目录刷新。第三步验证源头。清理了CDN缓存后回源是否可以正常返回正确的响应如果源站本身也挂了光刷新CDN缓存也无济于事反而可能导致用户从能显示但旧变成完全打不开。所以刷新动作之前务必先验证源站响应正常。第四步做原因分析。查清楚是配置变更、程序bug还是文件丢失导致的问题落到文档里避免下次重蹈覆辙。这里有一个很实在的提醒不要频繁动用全量刷新。我见过一个团队为了图省事每次发版都把CDN全量刷新一遍——对流量大的站点来说全量刷新会把CDN节点全部打到回源源站瞬时流量可能暴涨几倍甚至十几倍。尤其是高峰期这样的操作很可能把源站打挂。尽量做到按需刷新、最小粒度刷新。6.4 缓存策略的定期审视缓存配置不是写完就一劳永逸的。页面结构调整了、资源变多了、CDN产品换了、甚至用户流量分布变化了都会影响到之前的配置是否还合适。我建议每季度做一次缓存配置的审视重点看三个指标一是静态资源命中率是否还停留在高位。如果下滑看看是不是有团队绕过了配置中心、私自改了URL格式。二是HTML的校验频率是否合理。如果源站日志显示HTML的GET请求量猛增说明可能存在缓存穿透检查是否有恶意请求或配置了no-store但没有配合CDN缓存。三是兜底逻辑是否正常。Service Worker版本是否覆盖了大多数活跃用户旧版本是否清理完毕。每次审视后把调整过的配置和原因记录到文档里。时间长了这份记录就会成为团队内部的缓存知识库比任何外部资料都更有参考价值。7. 资源加载的整体优化编排把前置、缓存和构建串起来最后把这节的内容放到更大的优化上下文里看一看。前置资源和缓存的方案都必须和构建产物设计配合才会有最佳效果。前端构建产物怎么做直接决定你前置什么、缓存多久。7.1 构建产物的拆包策略决定了缓存的价值一个常见的误区是把所有JS打包成一个app.js然后给它设置长缓存。这个方案在代码不频繁变动时还能用一旦项目迭代快了每次改一行代码都会让整个app.js变化hash跟着变用户被迫重新下载所有JS缓存的收益几乎为零。正确的做法是拆包。把不常变动的第三方库框架层、工具库单独打包成vendor.js把公共业务代码打包成common.js把每个业务页面的代码按路由拆成独立chunk。这样改动只影响它所属的那个chunk其余资源都能继续命中缓存。我见过一个从单体打包改为拆包的项目改造案例改造前每次发版用户需要重新下载大约700KB的JS改造后同样一次发版用户只需要下载约30KB的变更chunk。缓存命中率提升带来的性能改善比任何压缩手段都明显。7.2 资源指纹的生成策略要注意什么资源指纹文件名里的hash生成主流的方案主要有两类。一类是内容hashcontenthash根据文件内容生成内容变了hash才变这是静态资源的标准做法。另一类是生成时间戳或者版本号这种做法会导致文件内容没变但URL也变了缓存完全失效不建议用于静态资源。还有一类容易踩坑的场景是多个入口页面引用了同一个公共模块。如果公共模块的hash生成不做统一处理可能出现同一个模块在不同页面生成不同hash的情况导致用户在页面间跳转时重复下载同一个模块的内容。打包配置里需要做好公共模块的抽取和缓存组策略保证同一份代码在全局hash一致。7.3 前置资源和拆包策略的配合顺序有了合理的拆包结果前置资源的编排才有意义。我的推荐顺序是这样的先保证HTML中的引用顺序正确、核心脚本以正常方式加载不要全部变为async/defer否则首屏渲染时机不可控然后用preload把首屏必经的字体、关键CSS、关键图片加载时机提前用prefetch把用户最可能去的下一个页面的资源提前加载最后用dns-prefetch和preconnect把跨域接口域名的连接时间隐去。这个顺序执行完之后整个页面的加载链路应当是HTML一到关键资源立刻并行开跑次要资源等到HTML解析到引用处才加载下一个可能被访问的资源则在浏览器空闲时预取等待用户跳转时几乎没有等待时间。这里的兜底逻辑同样要清楚preload加载的资源仍然受同域名并发连接数HTTP/1.1是6个左右限制如果同时preload了10个资源后面的还是要排队。HTTP/2的并发能力更宽松一些但如果资源数量太多CDN和浏览器依然会有限制。所以preload不是越多越好挑性价比最高的3到5个资源就可以了。7.4 性能验证的完整闭环每次做完前置资源和缓存的调整都需要用数据验证收益不能靠感觉。验证一般分两层本地性能测试主要用Lighthouse之类的工具跑性能评分看First Contentful Paint、Largest Contentful Paint和Speed Index这些指标的变化。值得注意的一点是本地多跑两遍能直观看到二次加载的收益但也要知道Lighthouse的测试环境是模拟的和真实用户环境有差异。线上验证则要通过真实用户监控数据RUM来看。对比发布前后同时间段、同地区、同网络类型下的首屏耗时分布。如果发现优化后某个地区的长尾P95耗时没有明显下降可能和CDN节点覆盖有关系这种时候前置资源能起到的作用有限需要考虑CDN层面的问题。我个人的习惯是每次缓存或前置资源调整都留下前后两段可对比的数据并且把结论写进项目的性能看板里。优化工作最忌讳的是一次性的、不可持续的操作只有当每一次调整都能被数据验证、被文档记录整个团队的能力才会稳定积累起来。从我自己的实操体会来看前置资源和缓存这块内容不要等线上出了问题才想起来做。在一开始设计页面架构时就把加载时序、缓存策略规划进去成本是极低的但收益却是持续性的。每次发布、每次迭代用户都在享受这套基础方案带来的提速效果。