ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

按下F5后浏览器到底做了什么?从缓存到渲染的完整链路解析

按下F5后浏览器到底做了什么?从缓存到渲染的完整链路解析 你肯定按过无数次F5。写完代码刷新看效果页面转圈时不信邪地狂按甚至开会演示时手在键盘上摸到F5都会下意识敲一下。但很少有人停下来想过这一下按键到底触发了什么我最早意识到这个问题不简单是在一次线上事故排查里。前端同学说“我改了代码刷新了好几次都没生效”后端同学说“我接口改了字段你这边还是旧数据”。两边都觉得自己没错最后查下来全是缓存和条件请求在捣鬼。从那时起我养成一个习惯任何涉及页面加载的讨论先搞清楚F5那一下到底经历了什么。这篇文章想带你完整走一遍这条链路从缓存检查、条件请求到DNS、TCP握手再到HTML解析、样式计算、布局绘制最后分享我日常调试中踩过的一堆坑。前端、测试、运维甚至产品经理看完都能对“刷新”这件事建立一套完整认知模型。1. 按下F5的第一秒浏览器先检查的永远是缓存1.1 普通刷新、强制刷新和无痕刷新藏着完全不同的策略很多人以为按F5等于“重新下载页面”这个理解在浏览器设计者眼里是不成立的。真实情况是浏览器收到F5指令后第一件事不是发起网络请求而是翻自己本地的“小账本”——缓存。按普通F5浏览器会先检查每个资源有没有缓存以及缓存是否还在新鲜期内。如果资源新鲜直接拿出来用连服务器都不打扰。这就是你在Network面板里看到“from disk cache”或“from memory cache”的原因这根本不是一次真正的网络请求。强制刷新就不一样了。Windows上按CtrlF5Mac上是CmdShiftR浏览器会绕开强缓存那一层带着条件请求头去问服务器“这个资源到底变没变”。服务器如果判断没变会返回304浏览器继续用缓存内容如果变了就返回200和新的资源内容。注意强制刷新不是“清空缓存重新下载全部”而是“跳过本地新鲜度判断强制向服务器验证一次”。还有一个经常被误解的工具DevTools里的Disable cache选项。勾选它之后只要DevTools面板开着所有请求都不读缓存每次都走完整的条件验证流程。它非常适合调试静态资源但关掉DevTools就失效了别指望它能帮你清理线上环境的旧资源。无痕窗口又是另一种情况。它启动时是“干净”的没有任何缓存所以能模拟第一次访问页面时的状态但它不会帮你验证强缓存和后端资源的交互逻辑。很多小同学用无痕窗口代替强制刷新其实是用错的。这几种刷新方式的差异我整理成了一张表刷新方式是否读取强缓存是否发送条件请求典型场景普通F5是过期时才发日常刷新看效果CtrlF5 / CmdShiftR否是改了静态资源但F5没生效DevTools Disable cache否是需要连续验证多轮请求无痕窗口启动时无缓存视资源而定模拟首次访问搞清楚这些区别排查很多“改了没生效”的问题会快得多。1.2 缓存为什么要分层内存缓存与磁盘缓存的分工你可能会问浏览器直接把资源存下来不就行了为什么要分内存和磁盘这是取舍问题。内存缓存读起来极快几乎不消耗IO但内存是稀缺资源标签页关闭后容量就释放了不适合放大量资源。磁盘缓存容量大、可持久保存但访问速度比内存慢一个量级。浏览器会根据资源体积、类型、使用频率做动态判断。同一张图片你快速刷新页面时大概率出现在“from memory cache”里过几分钟再刷新可能就变成“from disk cache”了。这里有个实践要点如果你想确认某个资源到底有没有真正走网络不要只看是否显示“from cache”要区分是memory还是disk。两者的判定路径不同出现的位置也反映了不同的复用机制。我举个具体的例子。开发中经常遇到修改了CSS文件按F5后页面还是旧样式但Network面板里明明显示这个CSS请求是200不是from cache。这种情况通常不是缓存头配置的问题而是你改了磁盘上文件但页面短时间内被内存缓存拦住了浏览器根本没去磁盘上找新文件。解决办法很简单普通F5不行就强制刷新一次或者把DevTools的Disable cache打开再刷新。1.3 四个响应头决定资源“活多久”聊缓存不能绕开HTTP响应头。真正决定一个资源能活多久的是服务器随响应返回的那几个字段。最简单的是Expires它是一个绝对时间比如“2026-01-01 12:00:00”意思是这个时间之前都可以直接用缓存。但它有个硬伤依赖客户端时间用户把系统时间调错了缓存策略就乱了。现代方案是Cache-Control。它的max-age指令用相对时间表示新鲜期比如Cache-Control: max-age31536000表示一年内不用回源。如果Cache-Control和Expires同时存在Cache-Control优先这是HTTP规范里明确规定的。还有一个高频坑点Cache-Control: no-cache和no-store完全不是一回事。no-cache不是不缓存而是每次使用前都必须向服务器验证服务器说没变才继续用缓存no-store才是不允许缓存。很多生产事故就是因为把no-cache写成了no-store导致静态资源每次全量下载服务器带宽被打爆。除了这两个老协议里的Pragma: no-cache有时也会出现它是HTTP/1.0的产物兼容旧代理服务器用的优先级低于Cache-Control。真实项目中你只需要关心Cache-Control和Expires其他字段可以不做重点分析。2. 缓存没命中时浏览器才真正开始“请求”2.1 协商缓存带着筹码去问服务器强缓存过期了或者资源本来就没有缓存浏览器会发起一次完整的HTTP请求。但它不是机械地“下载全部内容”而是先试探性地问服务器“我手上有个旧版本你那边的内容变了没”这个试探就靠请求头里的两个字段实现。一个是If-Modified-Since它对应服务器响应头里的Last-Modified就是资源的最后修改时间。服务器收到后会比对一下如果文件在那个时间之后没改过就返回304 Not Modified响应体为空浏览器接着用本地缓存。这个方案实现简单但存在明显缺陷修改时间粒度是秒级同一秒内改了两次文件就判断不出来另外文件改了但内容没变时时间戳也会变导致白下载一次。另一个字段是If-None-Match它对应ETag是服务器根据文件内容生成的指纹标识。内容变了指纹才变没变指纹保持一致。浏览器会带着这个指纹去问服务器服务器比对后发现没变返回304变了就返回200和新内容。ETag比Last-Modified精确得多所以HTTP规范规定If-None-Match优先级高于If-Modified-Since两者都带时以后者为准。这里多说一句很多人的误区看到304不要以为是出错了它是正常的缓存验证结果。304不是告诉你“没有内容”而是告诉你“你已有的那份就是最新的别重复传输了”。这是节省带宽的经典设计跟错误没有任何关系。2.2 URL到连接DNS、TCP与TLS的三重握手资源确认要重新下载后浏览器才走到网络层。这一层的链路每一步都值得单独拿出来说。先从DNS说起。浏览器拿到域名后会依次查浏览器自身DNS缓存、操作系统DNS缓存、hosts文件如果都没有才会把请求交给系统配置的DNS服务器。到了DNS服务器这层如果它本地没有记录还会继续向上级查询最终沿着“根域名服务器 → 顶级域名服务器 → 权威域名服务器”这条链找到对应IP。整个过程中任何一层命中了缓存都会明显缩短解析时间。这也是为什么你第一次访问一个站点普遍比后续访问慢一点点DNS缓存热了之后解析过程几乎可以忽略不计。拿到IP后浏览器要和服务器建立TCP连接。HTTP/1.1时代每次请求通常要经历三次握手客户端发SYN服务器回SYNACK客户端再回ACK。为什么非要三次简单理解是双方都要确认“你好我能听到你你也能听到我”。两次不够四次多余三次刚刚好。如果是HTTPS站TCP之后还要做TLS握手协商加密套件、交换证书、生成会话密钥。TLS 1.3把握手精简到一个往返但即便如此在TCP三次握手之上又多加了一次RTT首次访问的延迟就是这么一毫秒一毫秒堆出来的。所以你在做性能优化时一定要知道一次HTTPS请求在没有缓存的情况下至少要经过DNS解析、TCP握手、TLS握手、HTTP请求响应四次往返才轮到真正下载内容。对服务器极远的站点来说这部分时间往往比资源下载时间还长。讲到这里顺便提一句TCP连接复用。HTTP/1.1的Connection: Keep-Alive可以让同一个TCP连接承载多个请求避免为每个资源都做一次握手。HTTP/2更进一步在同一个连接上多路复用并发传输多个资源彻底解决了HTTP/1.1的队头阻塞问题。你在Network面板里看到大量请求并行发出底层可能就只是一条TCP连接。2.3 请求到达服务器之后状态码与响应头在说什么请求抵达服务器应用处理完返回HTTP响应。这一层的状态码和响应头是前后端最容易扯皮的地方。先看状态码。200表示请求成功并返回了完整内容304表示内容未修改前面说过了204表示请求成功但无内容常用于埋点上报这类场景206是部分内容视频拖动播放时常看到。前端同学最需要关注的依然是200和304这两个数字直接决定了页面是用新资源还是旧资源。响应头里值得看的字段也不少。Content-Type告诉浏览器这是什么类型Content-Length是实体大小Content-Encoding表示压缩方式gzip或brotliAccess-Control-Allow-Origin是跨域配置。当你发现某个资源在Network里显示“Failed to load”或者明明请求了却拿不到数据第一反应应该是看响应头而不是看控制台报错控制台的报错往往只是表象。还有一个特殊请求值得留意CORS预检。当你用fetch发起跨域请求而且请求类型不是简单请求比如Content-Type是application/json或者带了自定义头浏览器会先自动发一个OPTIONS请求这叫预检。服务器必须对OPTIONS返回正确的Access-Control-Allow-*响应头真正的POST或GET才会被放行。你看到Network里出现莫名其妙的OPTIONS请求不用慌不是业务代码发的是浏览器在“问路”。3. 拿到响应后从字节到像素的渲染管线拆解3.1 HTML变DOM、CSS变CSSOM浏览器如何读懂代码响应到了浏览器手里渲染管线正式启动。这一过程没有你想的那么玄本质上是一个“翻译”过程把十六进制字节流翻译成字符把字符翻译成令牌把令牌结构化DOM节点最后组装成DOM树。具体来说HTML字节流先按字符编码解码成文字然后交给HTML解析器做词法分析拆出开始标签、结束标签、属性这些令牌。解析器再根据标签的嵌套关系构建出DOM树比如里套最终会形成一个有父子关系的树状结构。这个过程边下载边解析不是等整个HTML都下载完才开始所以大文件的解析进度是流式的。CSS也是一样的流程最终产出CSSOM也就是CSS对象模型。它和DOM树是两棵独立的树但在渲染阶段会被合并使用。这里有一个很多初学者踩过的坑CSS会阻塞渲染。浏览器只有把CSSOM构建完才有足够信息计算每个元素的最终样式所以在CSSOM完成之前页面不会渲染任何东西。这就是为什么你把一个加载缓慢的CSS文件放在页面顶部时整个页面会一直白屏哪怕下面的HTML内容早就下载解析完了。script标签的位置也同理。传统script会阻塞DOM解析所以过去大家习惯把script放在body末尾。现在有了defer和async两个属性行为变得灵活了async是下载完立刻执行执行时阻塞解析defer是下载和解析并行但等整个文档解析完才执行。搞不清楚这两个区别的面试和实战都会吃亏。3.2 布局、绘制与合成像素是怎么画出来的DOM和CSSOM合并后浏览器先生成渲染树。这棵树只包含可见元素display:none的子节点不会进来head标签也不会在这里。接着就是布局阶段浏览器需要算清楚每个元素在视口里的确切位置和尺寸这一过程也叫回流或重排。布局完成之后进入绘制阶段。浏览器把每个元素的背景、边框、文字、阴影等视觉属性按顺序画到一个个图层上这一步叫栅格化。最后浏览器把所有图层合成为最终画面并交给显卡显示这就是合成。这三步的性能代价差别很大。布局是最贵的因为改宽高、改位置可能牵一发而动全身导致很多元素重新计算。绘制是第二贵的改颜色、改阴影需要重新画出像素。合成是最省钱的如果你只改transform或opacity动画可以在合成层完成浏览器甚至不需要碰布局和绘制这就是为什么现代前端极其推崇transform动画。回到F5这个场景按下F5后页面闪白本质上是资源下载、样式计算、布局、绘制这一整条管线从头开始跑。白屏时间长短直接取决于渲染链路哪一步被拖慢了。3.3 JavaScript执行与资源加载优先级决定首屏快慢的隐性因素渲染管线里还有一对隐形的对手主线程和资源加载器。JavaScript默认运行在浏览器主线程上而这同一条线程同时还要处理HTML解析、样式计算、布局和绘制。如果一个大型脚本在执行它就像占着马路不动的卡车后面的车全堵住。这就是为什么Performance面板里能看到“Long Task”超过50毫秒的任务就会标记出来用户感知到的卡顿往往就是这些长任务造成的。另一面浏览器有个叫预加载扫描器的组件会在HTML解析过程中提前发现带src或href的资源然后通知网络层并行下载。虽然JS执行会阻塞解析但下载在空闲时早就开始了。基于这种机制资源加载优先级也非常讲究CSS和脚本优先级最高图片其次懒加载资源最低。现代前端还引入了preload、prefetch这类资源提示。preload告诉浏览器这个资源当前页面就需要请尽快加载prefetch是浏览器空闲时提前拉取用户真跳到下个页面时秒开。还有loadinglazy属性让图片延迟到即将进入视口时才请求。这些手段组合起来能显著改善F5后的首屏体验。4. 刷新相关的常见问题与排查技巧4.1 页面改了但F5没变化缓存背锅还是后端背锅这是前端日常遇得最多的场景本地改了样式或脚本部署上去之后刷新还是旧样子。直接说结论大概率有三个坑。第一个是强缓存命中。服务器返回静态资源时带了Cache-Control: max-age31536000浏览器一年之内都不会重新下载你发再多的版本号都没用因为浏览器根本不发请求。遇到这种情况先打开Network面板看这个资源的状态如果显示“from disk cache”问题就锁定了。第二个是协商缓存逻辑不当。有些后端框架默认给静态资源返回了Last-Modified但文件确实改了Last-Modified因为某种原因没有更新导致服务器返回304前端看到的还是旧文件。这时可以用curl -I直接看响应头对比文件的修改时间问题一目了然。第三个经常被忽略Service Worker。如果站点注册了Service Worker刷新请求会先被SW的fetch事件拦截缓存策略完全由SW代码决定跟HTTP缓存头没有关系。强制刷新甚至都不能绕过它。排查时打开Application面板看Service Workers列表如果有注册的SW直接勾选“Update on reload”或Unregister马上就能验证。我在排查这类问题时习惯按固定的顺序来先Network面板看状态码再用curl看响应头最后检查Service Worker。三步走完九成问题都能定位。4.2 调试时怎么高效刷新而不被缓存坑平时开发最好养成一套固定的刷新策略别每次都CtrlF5乱按。本地开发阶段推荐直接依赖构建工具的dev server。Vite、webpack-dev-server都默认把缓存关掉而且有Hot Module Replacement改了代码只局部替换变更的部分页面不会整体重载开发体验接近“秒改秒见”。这种热更新本质是浏览器和dev server之间维持了一条WebSocket连接服务器推送“某文件已修改”前端收到消息后通过模块替换更新视图根本不走正常的HTTP刷新路径。调试线上或老项目时我更推荐组合拳DevTools开Network面板勾选Disable cache然后打开无痕窗口最后再按一次普通F5。这样既保证了初始缓存为空又保证了每次请求都带着条件头去验证不会出现“这次改了下次又没改”的假象。还有个小技巧我用了很多年调试时给请求地址手动加一个查询参数比如style.css?v123这个v参数只要变化就会导致资源URL不同浏览器会当它是新资源重新请求。这种方式改完参数、刷新一次就行不需要强制刷新也不影响其他资源的缓存命中非常适合临时验证单个文件。4.3 刷新后白屏、卡顿怎么定位F5之后页面白屏时间长或者刷新过程中明显卡顿不要凭感觉猜原因直接用Performance面板记录一次完整加载。操作方式是打开DevTools切到Performance面板点击刷新按钮录制完成后看时间线。重点关注几个指标。First Contentful Paint是首次出现内容的时间反映加载进度Largest Contentful Paint是最大内容元素可见的时间基本代表首屏视觉完成还有Long Task的数量超过200毫秒的长任务一多用户感知就是一顿一顿的。然后切到Network面板看水瀑布图。哪个资源的耗时最长哪条请求是串行等待的一眼就能看出来。常见的原因就那么几个某个脚本放在head里导致阻塞、一大堆图片没有懒加载、字体文件太大、服务端响应首字节时间太长、压缩没开。对应处理方法也很标准脚本加defer或async、图片懒加载、字体子集化、后端加缓存和压缩。这里有一个容易忽略的经验刷新卡顿不一定全是网络问题也可能是页面内存占用过高。刷新会销毁旧页面并创建新页面如果旧页面的大量对象没被正确释放就会卡在原页面的销毁阶段。这种情况用Performance面板看不出来需要把录制的范围拉长看刷新前后的内存曲线拐点。遇到类似的状况先排查全局事件监听器、定时器和闭包引用能解决大多数泄漏问题。5. 实操心得我处理缓存与刷新的几个默认规则5.1 三个能直接抄走的生产配置规则和团队沟通过无数次后我把生产环境的缓存配置收敛成了三条默认规则直接抄走能用。第一HTML文件不回源缓存用Cache-Control: no-cache确保每次访问都向服务器验证版本保证用户刷新到的首页永远是最新的入口文件。第二静态资源全部走内容指纹。文件名带上contenthash比如app.8f3a2b.js然后用Cache-Control: max-age31536000, immutable。文件名一变浏览器自然当成新资源请求文件名不变说明内容没变哪怕缓存一年也安全。这是现在Webpack、Vite默认的产物策略。第三接口统一支持协商缓存通过ETag或Last-Modified判断数据是否变化。这样能省去大量重复响应体传输又不会因为强缓存时间设得长而让前端拿到过期数据。接口的数据变化是高频的但也不是每次请求都有新内容协商缓存是最平衡的方案。这套规则的好处是把“不变的内容缓存到底变化的内容每次验证”两边都照顾到了。绝大多数“刷新没变化”的线上事故追根溯源都是没按这三条来。5.2 手动F5之外的工程化刷新方案现在的前端开发环境已经很少有人用纯手动F5来做日常调试了。Vite的HMR是Local Development Server里的标配改一个React组件保存瞬间页面局部热更新状态还不丢失。webpack-dev-server也提供了类似能力靠WebSocket通知前端“模块变了”前端再用模块热替换机制处理更新不会整页刷新。但工程化工具并不能完全替代手动F5。HMR偶尔会遗漏某些模块边界出现状态错乱这时候还得按一次刷新让页面彻底重载。Service Worker的更新也经常需要强制刷新两到三次才能看到最新效果因为SW本身的激活和页面接管有一个生命周期。还有跨域第三方资源、内嵌iframe里的内容这些都不在dev server掌控范围内只能老老实实手动刷新。我自己现在的习惯是开发时默认靠HMR遇到状态错乱或数据奇怪时强制刷新一次查线上问题时优先动DevTools的Disable cache和Network面板别一上来就狂F5。工具永远是辅助真正要搞清楚的是F5背后那几条链路到底卡在哪一环上。
返回列表