缓存配置和解析渲染
缓存配置和页面渲染
一、 HTTP缓存:强缓存 vs 协商缓存(怎么配、在哪配)
面试官会直接问:“给你一个新项目,你怎么配置静态资源缓存?” —— 你只需亮出这张表加一段 Nginx 配置,逻辑就极其清晰。
- 强缓存(
Cache-Control):直接从磁盘/内存读,不发请求。JS/CSS/图片(带指纹)用max-age=31536000, immutable(一年)。 - 协商缓存(
ETag/Last-Modified):发请求问服务器是否过期,没过期返回 304。index.html必须用no-cache, must-revalidate。
Nginx 配置示例(直接说给面试官听):
# 静态资源(带 hash 的 JS/CSS,如 main.a1b2c3.js)—— 强缓存
location ~* \.(js|css|png|jpg|svg|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML 文件 —— 协商缓存(必须回源校验)
location ~* \.html$ {
add_header Cache-Control "no-cache, must-revalidate";
}面试话术:“强缓存和协商缓存是配合关系。强缓存命中就不发请求;强缓存过期或没有强缓存,才走协商缓存对比
ETag,决定是返回 304 还是新资源。”
二、 SPA 的 index.html 为什么不能强缓存?(核心痛点)
面试官会追问:“SPA 项目中,index.html 用什么缓存策略?为什么?”
- 结论:绝对不能用强缓存! 必须用
Cache-Control: no-cache(协商缓存)或max-age=0。 - 原因:SPA 只有一个
index.html入口。如果它被强缓存,用户浏览器会一直使用旧版index.html,引用的依然是旧版 JS/CSS(main.old.js),但服务器可能早已删除旧文件(因为new.js文件名变了),导致页面直接白屏或报错 404。
最佳实践:
index.html:Cache-Control: no-cache, must-revalidate(每次都回源验证)。- JS/CSS 文件:带上 内容哈希(Content Hash),如
main.a1b2c3.js,设置强缓存一年。文件变了,哈希就变了,URL 就变了,用户自然请求新文件。
解释Nginx配置
第一步:先看“匹配规则”(location 是啥?)
配置块 1:匹配静态资源(图片、JS、CSS) location ~* .(js|css|png|jpg|svg|woff2)$ { ... }
逐词翻译(人话版):
location:告诉 Nginx,“当用户请求某个网址时,我要匹配这个网址”。~*:两个符号组合。~表示“使用正则表达式匹配”(高级模糊匹配)。*表示“不区分大小写”(JS和js都认)。
\.:在正则里,.代表任意字符,这里加了个反斜杠\,表示“我要匹配一个真实的点号”(比如文件名里的main.js中间那个点)。(js|css|png|...):竖线是“或”的意思。匹配这些后缀名。$:表示“结尾”。意思是网址必须以这些后缀结尾(防止匹配到main.js.bak这种奇怪文件)。
这块配置生效的场景:当浏览器请求 https://xxx.com/static/main.a1b2c3.js 或 logo.png 时,就会命中这个规则,进入这个大括号 {} 里执行指令。
第二步:命中静态资源后,执行什么指令?
expires 1y;
add_header Cache-Control "public, immutable";
expires 1y;:- 这是设置 HTTP 响应头里的
Expires字段(老规范)。 - 意思就是告诉浏览器:“这个东西一年后才过期”。在一年内,浏览器问都不会问你(服务器),直接拿本地缓存用。
- 这是设置 HTTP 响应头里的
add_header Cache-Control "public, immutable";:- 这是设置 HTTP/1.1 的
Cache-Control字段(新规范,优先级高于Expires)。 public:意思是“中间节点(比如 CDN、公司代理网关)也可以缓存这份文件”,这样离用户更近,加载更快。immutable(不变量):这是最强硬的指令。意思是“这个文件永远不变!”(因为带了 hash,如main.a1b2c3.js,变了一点 hash 就变了,文件名就变了,所以确实不会变)。浏览器收到这个指令,连“检验是否过期”的请求都彻底免了,绝对不跟服务器通信。
- 这是设置 HTTP/1.1 的
第三步:配置块 2(HTML 文件)
location ~* \.html$ {
add_header Cache-Control "no-cache, must-revalidate";
}
匹配规则:只要请求以
.html结尾(比如index.html、about.html),就执行这个块里的指令。add_header Cache-Control "no-cache, must-revalidate";:no-cache:名字有误导性,它不是“不缓存”。它的意思是:“每次必须去服务器验证一下(发个请求问问服务器:我的版本是不是最新的?)”。如果服务器说“你是最新的”(304),再读缓存;如果说“不是最新的”(200),再拉新文件。must-revalidate:增强指令。意思是“一旦过期,必须去源服务器(Nginx/后端)验证,不能随便找个中间代理去验证”。
第四步:实战推演(为什么要这么配?结合你的 SPA)
面试官如果问“为什么 HTML 不设置强缓存?”,你就这样解释:
“(指着配置) 你们看静态资源(
main.a1b2c3.js)因为文件名里有a1b2c3这个哈希值(版本号),代码变了,哈希值就变了,文件名就变成main.d4e5f6.js了。浏览器发现请求的 URL 变了,自然就会去拿新文件,所以原来的旧main.a1b2c3.js哪怕缓存一年也没事。但是
index.html没有哈希值。只要我发版改了代码,index.html里引用的 JS 路径就会从main.old.js变成main.new.js。如果我把index.html也设置成expires 1y,用户一年内都不会请求新的index.html,他的浏览器会一直拿着旧index.html,试图加载main.old.js,但服务器早就把main.old.js删了,页面直接白屏报错。所以
index.html必须设成no-cache,每次都要去服务器看一眼:我是不是最新的?”
第五步:怎么确认它生效了?
你可以补一句:“生产环境上线后,我会打开浏览器开发者工具的 Network(网络)面板,看一眼 index.html 的请求头(Response Headers)。如果看到 Cache-Control: no-cache,并且状态码是 304(说明走协商缓存),那就证明配置生效了。而静态资源(JS/CSS)如果看到 Cache-Control: public, immutable,并且状态码是 200 (from disk cache),说明强缓存生效了。”
带 hash 值的文件名
不是所有项目都“必须”有这两套配置,但这套“带 hash 的资源永久缓存 + index.html 协商缓存”的组合,是现代前端工程化项目的性能优化最佳实践。绝大多数正式上线的项目都会采用这个方案。
至于带 hash 值的文件名,它并不是手动修改的,而是由构建工具(如 Webpack、Vite)在打包时自动生成并注入的。下面我详细解释它是怎么实现的。
一、 带 hash 的文件名是怎么实现的?
这全靠构建工具在打包时的“文件指纹(File Fingerprinting)”功能。它的工作流程如下:
- 读取文件内容:构建工具会读取你的源代码文件。
- 计算哈希值:它基于文件的内容,通过一个哈希算法计算出一串唯一的哈希值。只要文件内容变了一个字符,生成的哈希值就完全不同。
- 重命名文件:工具会按照配置,将输出文件命名为
原文件名.哈希值.后缀的格式。 - 自动更新引用:最关键的一步!工具会自动更新
index.html中对该资源的引用路径。例如,<script src="main.js">会被自动替换为<script src="main.a1b2c3.js">。
二、 代码里是怎么配置的?(面试重点)
根据你使用的构建工具,配置方式略有不同。
1. Webpack 项目
在你的 webpack.config.js 文件中,通过 output 配置项里的 [contenthash] 占位符来实现。
// webpack.config.js
module.exports = {
// ...
output: {
// 在文件名中使用 [contenthash] 占位符
// 这样打包后就会生成类似 main.a1b2c3d4.js 的文件
filename: '[name].[contenthash].js',
// 对于通过代码分割(Code Splitting)生成的 chunk 文件也同样处理
chunkFilename: '[name].[contenthash].chunk.js',
},
};[contenthash] 是最佳实践,因为它只基于文件自身的内容生成哈希,内容不变,哈希就不变。
2. Vite 项目
Vite 在生产构建(npm run build)时,默认就会对静态资源文件名进行哈希处理。如果你需要自定义,可以在 vite.config.js 中配置:
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
output: {
// 自定义 JS 文件名格式,[hash] 占位符会被替换
entryFileNames: 'assets/[name].[hash].js',
chunkFileNames: 'assets/[name].[hash].js',
// 自定义 CSS/图片等静态资源文件名
assetFileNames: 'assets/[name].[hash].[ext]'
}
}
}
});可以看到,Vite 和 Webpack 的原理是一样的,都是通过占位符让构建工具自动生成带哈希的文件名。
三、 面试“连招”话术
你可以这样把这个问题讲给面试官听:
- “缓存配置是前端性能优化的标准实践,但不是所有项目都必须有。对于需要长期维护和迭代的正式项目,这套配置是必需的。”
- “带 hash 的文件名是由 Webpack 或 Vite 这类构建工具自动生成的,我们不需要手动修改。”
- “以 Webpack 为例,只需在
output配置中用[contenthash]占位符,它就会基于文件内容计算 hash。index.html中的引用路径也会被构建工具自动更新。” - “这样,文件内容变了,hash 变了,URL 就变了,浏览器就会认为是全新的资源,从而完美解决了版本更新后的缓存问题。”
三、 输入 URL 到页面渲染的“完整链条”(重点阐述渲染流程)
- 网络层:DNS 解析 -> TCP/TLS 握手 -> 发送 HTTP 请求 -> 接收响应。
- 解析层:HTML 解析成 DOM 树(遇到
<script>可能阻塞),CSS 解析成 CSSOM 树(CSS 阻塞渲染,但不阻塞 DOM 解析)。 - 渲染层(核心!):DOM + CSSOM 合并为 渲染树(Render Tree) -> 布局(Layout)(计算几何信息) -> 绘制(Paint)(填充像素) -> 合成(Composite)(GPU 图层合并)。
四、 对“具体元素”来说,布局和渲染做了什么?(深水区)
面试官会盯着一个元素问细节,这里别慌:
- 布局(Layout/Reflow):计算这个元素在屏幕上的确切几何信息(宽、高、
x、y、边距、边框)。它是递归的——父元素布局完,子元素才计算。 - 绘制(Paint):将布局算好的几何信息,填充颜色、文本、阴影、边框,生成一个个像素点。
- 合成(Composite):把不同的绘制层(比如
transform: translateZ(0)会触发新建层)交给 GPU 合成最终屏幕图像。
五、 display: none 的灵魂拷问(是区分“会用”和“懂原理”的分水岭)
面试官:“display: none 的元素,会被构建到渲染树(Render Tree)里吗?”
- 第一层回答:“不会。渲染树是由 DOM 树 和 CSSOM 树 合并时生成的,
display: none的元素会被过滤掉,它不占据任何空间,也不触发布局和绘制。” - 第二层回答(进阶):“但它依然存在于 DOM 树中! 只是被‘渲染树’踢出去了。通过
document.querySelector依然可以拿到它。它的子元素也会被一同过滤,即便子元素显式写了display: block,因为父元素被移除了。” - 终极对比(体现系统认知):
display: none:不在渲染树,不占位,不触发渲染。DOM 操作后触发回流。visibility: hidden:在渲染树,占位(几何空间还在),不可见,触发绘制。opacity: 0:在渲染树,占位,完全绘制(只是透明),触发合成(硬件加速)。
终极话术:“在我们 WeLink 的会话列表中,未读消息的气泡红点,我用
visibility: hidden控制,因为它占位,切换时不会导致旁边的文字左右跳动;而整个子列表的懒加载,我会用display: none,因为它彻底脱离文档流,能减少首屏 Layout 计算量。”
解释display: none
- DOM 树里有它(
document.querySelector能拿到,JS 能操作)。 - 渲染树(Render Tree)里绝对没有它。
一、 先画两条“生产线”(区别在哪里)
- DOM 树(Document Object Model):这是 “HTML 结构说明书”。浏览器读取 HTML 标签后,生成这棵树,它记录了页面有什么元素以及元素之间的层级关系(父子、兄弟)。它是 JS 操作的基础。
- 渲染树(Render Tree):这是 “装修施工图”。这棵树只包含**“需要画在屏幕上的东西”**。它是 DOM 树和 CSSOM(样式规则)结合后,过滤掉那些不需要展示的节点后生成的。
二、 display: none 在这两条线上的命运
1. 在 DOM 树上(存在) 浏览器解析 HTML 时,遇到了这个标签(比如 <div style="display:none">),它会直接把这个节点挂在 DOM 树上。所以你可以用 document.getElementById 找到它,也可以修改它的 className。
2. 在渲染树上(被“踢出局”) 当浏览器准备画页面时,它会遍历 DOM 树,逐个检查节点的样式。当它看到这个节点是 display: none 时,它会直接**“无视”它及其所有子孙后代**,不会把它们复制到渲染树里。
三、 我们“关注”的是“有还是没有”?(面试官最想听的逻辑)
既然它不渲染,那我们关注什么?关注成本(Cost)。
渲染树里没有它,意味着:
- 它不占位:它没有宽高,不会把旁边的文字挤开。
- 它不参与布局(Layout/Reflow):浏览器在计算所有元素几何位置时,完全跳过它。它就像不存在一样。
- 它不触发绘制(Paint):GPU 不会给它分配任何像素。
- 它的子节点也被连坐:哪怕子节点写了
display: block,只要父级是none,整个子树都会被从渲染树中移除。
那么,既然不渲染,我们还在乎它吗?在乎!—— 在乎它的“JS 操作成本”和“回流成本”:
- 它在 DOM 树里,占用内存(JS 堆内存)。如果 DOM 节点极其庞大,即使隐藏,也会占用内存。
- 当你把
display: none改成display: block时,这一步会触发 回流(Reflow)。因为渲染树里突然多了一大块节点出来,浏览器需要重新计算布局,这个代价很大。
四、 面试官必追的“对比三连击”(你顺便背下来)
面试官紧接着就会问:“那 visibility: hidden 呢?”
你的回答(直接封神):
“
display: none是物理移除,它在渲染树中彻底消失,不占位,兄弟元素会挤上来占它的位置。而
visibility: hidden是光学隐身,它依然存在于渲染树中,宽高位置都还在(占着茅坑),只是不画像素,所以它下面的元素不会移动。而且它的子元素如果设为visibility: visible,是可以显示出来的,但display: none的子元素永远显示不了。”
五、 终极“一句总结”(面试时作为收尾)
“面试官,所以我的理解是:
display: none元素在逻辑结构上(DOM 树)有,但在物理显示上(渲染树)无。我们关注它,主要是关注两点:第一,它是 JS 可寻址的,我们可以操作它;第二,它是 布局计算零成本的,但 切换显示时成本极高(会引发大面积回流)。因此,在 WeLink 这种长列表场景,我会用
visibility: hidden来占位隐藏占位图,而用display: none来彻底移除复杂的图表组件以释放内存。”