如果你是 Java 后端工程师,第一次接触前端里的 CDN,最容易产生两个疑问:
- 前端组件平时不是用
npm install吗,为什么又要提 CDN? - CDN 节点上的资源,到底是怎么从源站"拷贝"过去的?
这篇文章就围绕这两个问题展开。我们尽量不站在纯前端视角,而是用后端工程师更熟悉的方式来理解它。
一、先说结论
可以先记住这几个核心结论:
- 前端组件的主流分发方式通常是
npm,不是 CDN。 - CDN 主要用于分发浏览器直接访问的静态资源,比如
js、css、图片、字体、视频。 - 你一般不会把一个
js文件手动上传到全国所有 CDN 节点。 - 更常见的做法是:先上传到源站,然后由 CDN 节点在访问时回源拉取,再缓存到边缘节点。
- 如果不想让第一个访问用户承担回源延迟,可以额外做 CDN 预热。
二、npm 和 CDN 不是一回事
很多人刚接触前端时,会把 npm 和 CDN 混在一起。实际上它们解决的是两个不同层面的问题。
1. npm 解决的是"工程依赖管理"
这和 Java 后端里的 Maven、Gradle 很像。
比如你们公司开发了一个通用组件:
- 一个埋点 SDK
- 一个图表组件
- 一个内部 UI 组件库
业务项目通常会这样接入:
bash
npm install @your-company/monitor-sdk
然后在代码里这样使用:
js
import { initMonitor } from '@your-company/monitor-sdk'
这和后端项目通过依赖管理工具拉取 jar 包,本质上是一类事情。
2. CDN 解决的是"静态资源分发和加速"
当页面最终跑在浏览器里时,浏览器需要去下载:
app.jsvendor.jssdk.min.jsstyle.css- 图片、字体、视频
这些文件适合放到 CDN 上,因为它们:
- 内容通常对所有用户都一样
- 请求量大
- 体积可能不小
- 非常适合缓存
所以可以这样理解:
npm像 Maven 仓库,负责"给工程拉依赖"- CDN 像"全国分布式静态资源缓存网络",负责"给浏览器加速下载文件"
三、一个实际例子:把公司自研 SDK 发布到 CDN
假设你们公司开发了一个浏览器可直接使用的监控 SDK,产物是:
monitor.min.jsmonitor.min.css
你希望业务页面这样接入:
html
<script src="https://cdn.company.com/monitor-sdk/1.2.0/monitor.min.js"></script>
<link rel="stylesheet" href="https://cdn.company.com/monitor-sdk/1.2.0/monitor.min.css" />
那整个发布过程通常是这样的:
- 前端或 CI 把 SDK 打包成浏览器可直接使用的静态文件。
- 把这些文件上传到源站。
- 在 CDN 厂商控制台配置加速域名和源站地址。
- 把 CDN 域名的 DNS 解析指向 CDN 厂商提供的 CNAME。
- 业务页面改为引用 CDN 域名。
这里最关键的一点是:
你真正直接上传的地方,通常不是 CDN 边缘节点,而是源站。
这个源站常见是:
- 对象存储,比如 OSS、COS、S3
- 你们自己的 Nginx 静态资源服务器
四、先有源站,再有 CDN
很多初学者会以为"用了 CDN,就把文件直接传进 CDN 就行了"。这个理解不完全错,但容易忽略真正重要的架构关系。
更准确的理解是:
- 源站是资源的权威来源
- CDN 是分发和缓存层
源站上的路径可能是:
text
/monitor-sdk/1.2.0/monitor.min.js
/monitor-sdk/1.2.0/monitor.min.css
然后你在 CDN 控制台里配置:
- 加速域名:
cdn.company.com - 源站地址:
static-origin.company.com
这样之后,CDN 才知道它将来缓存未命中时,应该去哪里回源拉取文件。
五、CDN 厂商怎么对接
虽然不同厂商的控制台界面不一样,但本质流程很像。
1. 准备源站
源站必须能稳定提供静态文件访问,比如:
https://static-origin.company.com/monitor-sdk/1.2.0/monitor.min.js
2. 创建加速域名
你在 CDN 厂商后台创建一个域名,例如:
cdn.company.com
3. 配置回源地址
告诉 CDN:当某个节点没有这个文件时,去哪里拿。
例如:
- 源站域名:
static-origin.company.com
4. 配缓存规则
比如:
*.js缓存 30 天*.css缓存 30 天- 图片缓存更久
再配合源站返回的 HTTP 响应头:
Cache-ControlExpiresETagLast-Modified
5. 配 DNS
CDN 厂商会给你一个 CNAME 地址。你需要把:
cdn.company.com
解析到这个 CNAME 上。
完成之后,浏览器访问 cdn.company.com 时,流量就会先进 CDN 的调度系统。
6. CNAME 到底在这里起什么作用
很多人第一次接触 CDN 时,会把 CNAME 理解成"把域名指到源站"。
这其实是不对的。
在 CDN 场景下,CNAME 的真实作用是:
- 你自己的业务域名,先别直接解析到某台真实服务器 IP
- 而是先别名到 CDN 厂商提供的域名
- 再由 CDN 厂商的 DNS 调度系统,决定当前用户应该访问哪个 CDN 节点
可以把它理解成:
- 你的域名:
cdn.company.com - CDN 厂商给你的目标:
cdn.company.com.vendor-cdn.net
于是 DNS 上会有一条类似这样的记录:
text
cdn.company.com CNAME cdn.company.com.vendor-cdn.net
然后浏览器解析 cdn.company.com 时,大致过程是:
- 先查到它是一个 CNAME。
- 再继续解析
cdn.company.com.vendor-cdn.net。 - 这个解析过程进入 CDN 厂商自己的 DNS 调度系统。
- 厂商根据用户地域、运营商、网络质量、节点负载等因素,返回一个最合适的边缘节点 IP。
所以,CNAME 的作用不是"存资源",而是"把域名解析控制权交给 CDN 厂商,以便做节点调度"。
7. 源站、CNAME、CDN 节点三者到底是什么关系
这三者分别负责不同事情:
CNAME:负责把请求"导向 CDN 的调度体系"CDN 节点:负责接住用户请求,就近返回缓存内容源站:负责在 CDN 没有内容时,提供权威原始文件
如果用一句话概括:
CNAME 负责"指路",CDN 节点负责"分发和缓存",源站负责"提供原件"。
可以看下面这个关系图:
#mermaid-svg-Cq1lgVRgBDdBIRBF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Cq1lgVRgBDdBIRBF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Cq1lgVRgBDdBIRBF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Cq1lgVRgBDdBIRBF .error-icon{fill:#552222;}#mermaid-svg-Cq1lgVRgBDdBIRBF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Cq1lgVRgBDdBIRBF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Cq1lgVRgBDdBIRBF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Cq1lgVRgBDdBIRBF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Cq1lgVRgBDdBIRBF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Cq1lgVRgBDdBIRBF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Cq1lgVRgBDdBIRBF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Cq1lgVRgBDdBIRBF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Cq1lgVRgBDdBIRBF .marker.cross{stroke:#333333;}#mermaid-svg-Cq1lgVRgBDdBIRBF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Cq1lgVRgBDdBIRBF p{margin:0;}#mermaid-svg-Cq1lgVRgBDdBIRBF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Cq1lgVRgBDdBIRBF .cluster-label text{fill:#333;}#mermaid-svg-Cq1lgVRgBDdBIRBF .cluster-label span{color:#333;}#mermaid-svg-Cq1lgVRgBDdBIRBF .cluster-label span p{background-color:transparent;}#mermaid-svg-Cq1lgVRgBDdBIRBF .label text,#mermaid-svg-Cq1lgVRgBDdBIRBF span{fill:#333;color:#333;}#mermaid-svg-Cq1lgVRgBDdBIRBF .node rect,#mermaid-svg-Cq1lgVRgBDdBIRBF .node circle,#mermaid-svg-Cq1lgVRgBDdBIRBF .node ellipse,#mermaid-svg-Cq1lgVRgBDdBIRBF .node polygon,#mermaid-svg-Cq1lgVRgBDdBIRBF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Cq1lgVRgBDdBIRBF .rough-node .label text,#mermaid-svg-Cq1lgVRgBDdBIRBF .node .label text,#mermaid-svg-Cq1lgVRgBDdBIRBF .image-shape .label,#mermaid-svg-Cq1lgVRgBDdBIRBF .icon-shape .label{text-anchor:middle;}#mermaid-svg-Cq1lgVRgBDdBIRBF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Cq1lgVRgBDdBIRBF .rough-node .label,#mermaid-svg-Cq1lgVRgBDdBIRBF .node .label,#mermaid-svg-Cq1lgVRgBDdBIRBF .image-shape .label,#mermaid-svg-Cq1lgVRgBDdBIRBF .icon-shape .label{text-align:center;}#mermaid-svg-Cq1lgVRgBDdBIRBF .node.clickable{cursor:pointer;}#mermaid-svg-Cq1lgVRgBDdBIRBF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Cq1lgVRgBDdBIRBF .arrowheadPath{fill:#333333;}#mermaid-svg-Cq1lgVRgBDdBIRBF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Cq1lgVRgBDdBIRBF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Cq1lgVRgBDdBIRBF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Cq1lgVRgBDdBIRBF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Cq1lgVRgBDdBIRBF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Cq1lgVRgBDdBIRBF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Cq1lgVRgBDdBIRBF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Cq1lgVRgBDdBIRBF .cluster text{fill:#333;}#mermaid-svg-Cq1lgVRgBDdBIRBF .cluster span{color:#333;}#mermaid-svg-Cq1lgVRgBDdBIRBF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Cq1lgVRgBDdBIRBF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Cq1lgVRgBDdBIRBF rect.text{fill:none;stroke-width:0;}#mermaid-svg-Cq1lgVRgBDdBIRBF .icon-shape,#mermaid-svg-Cq1lgVRgBDdBIRBF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Cq1lgVRgBDdBIRBF .icon-shape p,#mermaid-svg-Cq1lgVRgBDdBIRBF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Cq1lgVRgBDdBIRBF .icon-shape .label rect,#mermaid-svg-Cq1lgVRgBDdBIRBF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Cq1lgVRgBDdBIRBF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Cq1lgVRgBDdBIRBF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Cq1lgVRgBDdBIRBF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
用户浏览器
访问 cdn.company.com
DNS 发现 CNAME
CDN 厂商调度系统
就近 CDN 边缘节点
节点是否已有缓存?
直接返回静态资源
回源到源站拉取文件
节点写入缓存
这个图里最重要的点有两个:
CNAME不会把文件存起来,它只是让请求先进入 CDN 厂商的调度链路。- 真正从源站"拷贝"文件到 CDN 节点的动作,发生在节点缓存未命中之后的回源阶段。
8. 为什么不能直接把域名解析到源站 IP
如果你直接这样配:
text
cdn.company.com -> 源站服务器 IP
那请求就会直接到源站,CDN 就根本接不住这个流量,也没法做:
- 就近调度
- 边缘缓存
- 抗带宽压力
- 热点文件分发
所以从接入关系上看:
- 配 CNAME 到 CDN 厂商域名,表示"先走 CDN"
- CDN 节点缓存 miss 后再回源,表示"源站作为最后的权威来源"
这也是为什么我们常说:
源站在 CDN 后面,CDN 在用户前面。
六、最关键的问题:源站的文件是怎么"拷贝"到 CDN 节点上的
这是最值得重点理解的地方。
结论先说
默认情况下,CDN 不是你一发布文件,它就自动立刻复制到全球每一个边缘节点。
更常见的是:
- 用户请求某个资源。
- 就近的 CDN 节点发现自己没有这个资源。
- 这个节点回源到你的源站拉取文件。
- 拉取成功后,节点把文件缓存下来。
- 后面再有用户访问这个节点时,直接从节点返回。
所以,"源站内容进入 CDN 节点"的动作,本质上通常是:
缓存未命中时的回源拉取。
七、默认模式:访问时回源缓存
下面用时序图看一下整个过程。
1. 发布和接入 CDN
"DNS" "CDN 厂商控制台" "源站(OSS / S3 / Nginx)" "构建系统" "开发者 / CI" "DNS" "CDN 厂商控制台" "源站(OSS / S3 / Nginx)" "构建系统" "开发者 / CI" #mermaid-svg-UCOeVhHzoz6Tdjwa{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UCOeVhHzoz6Tdjwa .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UCOeVhHzoz6Tdjwa .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UCOeVhHzoz6Tdjwa .error-icon{fill:#552222;}#mermaid-svg-UCOeVhHzoz6Tdjwa .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UCOeVhHzoz6Tdjwa .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UCOeVhHzoz6Tdjwa .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UCOeVhHzoz6Tdjwa .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UCOeVhHzoz6Tdjwa .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UCOeVhHzoz6Tdjwa .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UCOeVhHzoz6Tdjwa .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UCOeVhHzoz6Tdjwa .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UCOeVhHzoz6Tdjwa .marker.cross{stroke:#333333;}#mermaid-svg-UCOeVhHzoz6Tdjwa svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UCOeVhHzoz6Tdjwa p{margin:0;}#mermaid-svg-UCOeVhHzoz6Tdjwa .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UCOeVhHzoz6Tdjwa text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-UCOeVhHzoz6Tdjwa .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-UCOeVhHzoz6Tdjwa .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-UCOeVhHzoz6Tdjwa .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-UCOeVhHzoz6Tdjwa .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-UCOeVhHzoz6Tdjwa #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-UCOeVhHzoz6Tdjwa .sequenceNumber{fill:white;}#mermaid-svg-UCOeVhHzoz6Tdjwa #sequencenumber{fill:#333;}#mermaid-svg-UCOeVhHzoz6Tdjwa #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-UCOeVhHzoz6Tdjwa .messageText{fill:#333;stroke:none;}#mermaid-svg-UCOeVhHzoz6Tdjwa .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UCOeVhHzoz6Tdjwa .labelText,#mermaid-svg-UCOeVhHzoz6Tdjwa .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-UCOeVhHzoz6Tdjwa .loopText,#mermaid-svg-UCOeVhHzoz6Tdjwa .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-UCOeVhHzoz6Tdjwa .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-UCOeVhHzoz6Tdjwa .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-UCOeVhHzoz6Tdjwa .noteText,#mermaid-svg-UCOeVhHzoz6Tdjwa .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-UCOeVhHzoz6Tdjwa .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UCOeVhHzoz6Tdjwa .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UCOeVhHzoz6Tdjwa .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UCOeVhHzoz6Tdjwa .actorPopupMenu{position:absolute;}#mermaid-svg-UCOeVhHzoz6Tdjwa .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-UCOeVhHzoz6Tdjwa .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UCOeVhHzoz6Tdjwa .actor-man circle,#mermaid-svg-UCOeVhHzoz6Tdjwa line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-UCOeVhHzoz6Tdjwa :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 到这里为止,文件主要还在源站\nCDN 已经接入,但边缘节点不一定已有缓存 打包组件,产出 "monitor.min.js" 1 上传 "/monitor-sdk/1.2.0/monitor.min.js" 2 创建加速域名 "cdn.company.com" 3 配置源站地址 "static-origin.company.com" 4 配置缓存规则 5 返回 CNAME 地址 6 将 "cdn.company.com" CNAME 到 CDN 7
2. 第一次访问触发回源
"源站(OSS / S3 / Nginx)" "就近 CDN 边缘节点" "DNS / CDN 调度" "浏览器" "源站(OSS / S3 / Nginx)" "就近 CDN 边缘节点" "DNS / CDN 调度" "浏览器" #mermaid-svg-f7T7q8vJOXsCQmAI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-f7T7q8vJOXsCQmAI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-f7T7q8vJOXsCQmAI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-f7T7q8vJOXsCQmAI .error-icon{fill:#552222;}#mermaid-svg-f7T7q8vJOXsCQmAI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-f7T7q8vJOXsCQmAI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-f7T7q8vJOXsCQmAI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-f7T7q8vJOXsCQmAI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-f7T7q8vJOXsCQmAI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-f7T7q8vJOXsCQmAI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-f7T7q8vJOXsCQmAI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-f7T7q8vJOXsCQmAI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-f7T7q8vJOXsCQmAI .marker.cross{stroke:#333333;}#mermaid-svg-f7T7q8vJOXsCQmAI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-f7T7q8vJOXsCQmAI p{margin:0;}#mermaid-svg-f7T7q8vJOXsCQmAI .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-f7T7q8vJOXsCQmAI text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-f7T7q8vJOXsCQmAI .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-f7T7q8vJOXsCQmAI .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-f7T7q8vJOXsCQmAI .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-f7T7q8vJOXsCQmAI .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-f7T7q8vJOXsCQmAI #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-f7T7q8vJOXsCQmAI .sequenceNumber{fill:white;}#mermaid-svg-f7T7q8vJOXsCQmAI #sequencenumber{fill:#333;}#mermaid-svg-f7T7q8vJOXsCQmAI #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-f7T7q8vJOXsCQmAI .messageText{fill:#333;stroke:none;}#mermaid-svg-f7T7q8vJOXsCQmAI .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-f7T7q8vJOXsCQmAI .labelText,#mermaid-svg-f7T7q8vJOXsCQmAI .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-f7T7q8vJOXsCQmAI .loopText,#mermaid-svg-f7T7q8vJOXsCQmAI .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-f7T7q8vJOXsCQmAI .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-f7T7q8vJOXsCQmAI .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-f7T7q8vJOXsCQmAI .noteText,#mermaid-svg-f7T7q8vJOXsCQmAI .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-f7T7q8vJOXsCQmAI .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-f7T7q8vJOXsCQmAI .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-f7T7q8vJOXsCQmAI .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-f7T7q8vJOXsCQmAI .actorPopupMenu{position:absolute;}#mermaid-svg-f7T7q8vJOXsCQmAI .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-f7T7q8vJOXsCQmAI .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-f7T7q8vJOXsCQmAI .actor-man circle,#mermaid-svg-f7T7q8vJOXsCQmAI line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-f7T7q8vJOXsCQmAI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt "缓存命中" "缓存未命中" 这一步就是"源站文件拷贝到 CDN 节点"\n本质是节点在 miss 时回源并缓存 解析 "cdn.company.com" 1 返回最近的 CDN 节点 IP 2 请求 "/monitor-sdk/1.2.0/monitor.min.js" 3 检查本地缓存 4 直接返回 JS 5 回源拉取 "/monitor-sdk/1.2.0/monitor.min.js" 6 返回 JS 文件 7 按缓存规则写入本地缓存 8 返回 JS 文件 9
3. 后续访问直接命中缓存
"同一个 CDN 边缘节点" "同区域另一个浏览器" "同一个 CDN 边缘节点" "同区域另一个浏览器" #mermaid-svg-o3m9nXSJNeTexmSY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-o3m9nXSJNeTexmSY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-o3m9nXSJNeTexmSY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-o3m9nXSJNeTexmSY .error-icon{fill:#552222;}#mermaid-svg-o3m9nXSJNeTexmSY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-o3m9nXSJNeTexmSY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-o3m9nXSJNeTexmSY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-o3m9nXSJNeTexmSY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-o3m9nXSJNeTexmSY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-o3m9nXSJNeTexmSY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-o3m9nXSJNeTexmSY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-o3m9nXSJNeTexmSY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-o3m9nXSJNeTexmSY .marker.cross{stroke:#333333;}#mermaid-svg-o3m9nXSJNeTexmSY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-o3m9nXSJNeTexmSY p{margin:0;}#mermaid-svg-o3m9nXSJNeTexmSY .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-o3m9nXSJNeTexmSY text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-o3m9nXSJNeTexmSY .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-o3m9nXSJNeTexmSY .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-o3m9nXSJNeTexmSY .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-o3m9nXSJNeTexmSY .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-o3m9nXSJNeTexmSY #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-o3m9nXSJNeTexmSY .sequenceNumber{fill:white;}#mermaid-svg-o3m9nXSJNeTexmSY #sequencenumber{fill:#333;}#mermaid-svg-o3m9nXSJNeTexmSY #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-o3m9nXSJNeTexmSY .messageText{fill:#333;stroke:none;}#mermaid-svg-o3m9nXSJNeTexmSY .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-o3m9nXSJNeTexmSY .labelText,#mermaid-svg-o3m9nXSJNeTexmSY .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-o3m9nXSJNeTexmSY .loopText,#mermaid-svg-o3m9nXSJNeTexmSY .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-o3m9nXSJNeTexmSY .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-o3m9nXSJNeTexmSY .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-o3m9nXSJNeTexmSY .noteText,#mermaid-svg-o3m9nXSJNeTexmSY .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-o3m9nXSJNeTexmSY .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-o3m9nXSJNeTexmSY .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-o3m9nXSJNeTexmSY .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-o3m9nXSJNeTexmSY .actorPopupMenu{position:absolute;}#mermaid-svg-o3m9nXSJNeTexmSY .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-o3m9nXSJNeTexmSY .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-o3m9nXSJNeTexmSY .actor-man circle,#mermaid-svg-o3m9nXSJNeTexmSY line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-o3m9nXSJNeTexmSY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 请求同一个 "monitor.min.js" 1 检查缓存 2 直接返回命中结果 3
这就意味着:
- 第一个用户可能会承担一次回源延迟
- 后面的用户通常会更快
- 缓存是分节点、分区域存在的,不是"全球一次缓存,全球同时命中"
八、如果不想让首个用户变慢,可以做 CDN 预热
有些场景下,你不希望第一个真实用户触发回源,比如:
- 新版本刚发布
- 页面首屏很敏感
- 活动即将开始,流量会瞬间放大
这时可以使用 CDN 的预热能力。
预热的意思是:
- 发布完成后,主动通知 CDN 提前请求这些 URL
- 让 CDN 节点先把资源拉下来并缓存
- 等真实用户访问时,尽量直接命中缓存
需要注意的是,不同厂商的"预热"能力和范围不同:
- 有的按 URL 预热
- 有的按目录预热
- 有的会分区域执行
- 不一定代表全球所有边缘节点都被完全预热
它的本质仍然不是"你手工复制文件到每个节点",而是:
你主动触发一次或一批回源请求,让缓存提前建立。
九、默认回源和主动预热的对比
"真实用户" "源站" "CDN 节点" "CDN 平台" "发布系统 / CI" "真实用户" "源站" "CDN 节点" "CDN 平台" "发布系统 / CI" #mermaid-svg-4xxkEEOwzY3RwnRR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4xxkEEOwzY3RwnRR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4xxkEEOwzY3RwnRR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4xxkEEOwzY3RwnRR .error-icon{fill:#552222;}#mermaid-svg-4xxkEEOwzY3RwnRR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4xxkEEOwzY3RwnRR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4xxkEEOwzY3RwnRR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4xxkEEOwzY3RwnRR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4xxkEEOwzY3RwnRR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4xxkEEOwzY3RwnRR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4xxkEEOwzY3RwnRR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4xxkEEOwzY3RwnRR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4xxkEEOwzY3RwnRR .marker.cross{stroke:#333333;}#mermaid-svg-4xxkEEOwzY3RwnRR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4xxkEEOwzY3RwnRR p{margin:0;}#mermaid-svg-4xxkEEOwzY3RwnRR .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4xxkEEOwzY3RwnRR text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-4xxkEEOwzY3RwnRR .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4xxkEEOwzY3RwnRR .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-4xxkEEOwzY3RwnRR .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-4xxkEEOwzY3RwnRR .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-4xxkEEOwzY3RwnRR #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-4xxkEEOwzY3RwnRR .sequenceNumber{fill:white;}#mermaid-svg-4xxkEEOwzY3RwnRR #sequencenumber{fill:#333;}#mermaid-svg-4xxkEEOwzY3RwnRR #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-4xxkEEOwzY3RwnRR .messageText{fill:#333;stroke:none;}#mermaid-svg-4xxkEEOwzY3RwnRR .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4xxkEEOwzY3RwnRR .labelText,#mermaid-svg-4xxkEEOwzY3RwnRR .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-4xxkEEOwzY3RwnRR .loopText,#mermaid-svg-4xxkEEOwzY3RwnRR .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-4xxkEEOwzY3RwnRR .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4xxkEEOwzY3RwnRR .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-4xxkEEOwzY3RwnRR .noteText,#mermaid-svg-4xxkEEOwzY3RwnRR .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-4xxkEEOwzY3RwnRR .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4xxkEEOwzY3RwnRR .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4xxkEEOwzY3RwnRR .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4xxkEEOwzY3RwnRR .actorPopupMenu{position:absolute;}#mermaid-svg-4xxkEEOwzY3RwnRR .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-4xxkEEOwzY3RwnRR .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4xxkEEOwzY3RwnRR .actor-man circle,#mermaid-svg-4xxkEEOwzY3RwnRR line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-4xxkEEOwzY3RwnRR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 方案一:默认模式 方案二:发布后主动预热 首次请求 "monitor.min.js" 1 检查缓存 2 miss 后回源拉取 3 返回文件 4 返回文件并建立缓存 5 提交预热 URL 6 通知节点预取 7 提前回源拉取文件 8 返回文件 9 提前建立缓存 10 真实用户随后请求 11 直接命中缓存 12
十、实际工作里最容易踩的坑
1. 以为更新文件后,所有 CDN 节点会立刻同步
默认不是这样。很多时候仍然依赖:
- 缓存过期
- 主动刷新缓存
- 预热新版本
2. 文件内容变了,但 URL 不变
如果你一直覆盖:
/monitor-sdk/latest/monitor.min.js
那浏览器缓存、CDN 缓存、代理缓存都可能出现旧内容残留。
更推荐的方式是使用:
- 版本号路径,比如
/1.2.0/ - 或文件名 hash,比如
monitor.a83f21.min.js
3. 把 CDN 和 npm 当成二选一
实际上两者经常同时存在:
- 给工程集成时,发 npm 包
- 给浏览器 script 标签直连时,发 CDN 静态文件
4. 误以为所有开源前端组件都直接托管在 CDN 上
不是。大多数现代前端组件的主分发渠道依然是 npm。
你平时业务项目里安装 React、Vue、Ant Design、Lodash,更常见的是:
bash
npm install react
npm install vue
npm install lodash
至于浏览器里看到的 CDN 版本,很多时候只是某些公共 CDN 服务把 npm 包再转换成了可直接访问的静态文件地址。
十一、从后端视角怎么类比 CDN
如果一定要用后端工程师熟悉的语言来总结,可以这样类比:
- 源站像你的文件服务或对象存储,是资源的权威来源
- CDN 像部署在全国多地的静态代理缓存层
- 边缘节点像离用户最近的缓存代理
- 回源像缓存 miss 之后去上游取数据
- 预热像上线前先把热点数据加载进缓存
所以 CDN 并不神秘,它本质上就是:
把适合缓存的静态内容,尽量前置到离用户更近的位置。
十二、总结
把这篇文章压缩成一句话,就是:
前端组件通常通过 npm 分发给工程使用,而浏览器最终加载的 js、css 等静态资源则适合通过 CDN 加速;CDN 节点上的资源通常不是人工逐个上传,而是在缓存未命中时通过回源从源站拉取并缓存,必要时再配合预热减少首访延迟。
如果你已经理解了这件事,那么你对前端里 CDN 的认知就已经很接近生产实践了。