前端里的 CDN 到底是什么:从 npm、源站到边缘节点缓存

如果你是 Java 后端工程师,第一次接触前端里的 CDN,最容易产生两个疑问:

  1. 前端组件平时不是用 npm install 吗,为什么又要提 CDN?
  2. CDN 节点上的资源,到底是怎么从源站"拷贝"过去的?

这篇文章就围绕这两个问题展开。我们尽量不站在纯前端视角,而是用后端工程师更熟悉的方式来理解它。

一、先说结论

可以先记住这几个核心结论:

  • 前端组件的主流分发方式通常是 npm,不是 CDN。
  • CDN 主要用于分发浏览器直接访问的静态资源,比如 jscss、图片、字体、视频。
  • 你一般不会把一个 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.js
  • vendor.js
  • sdk.min.js
  • style.css
  • 图片、字体、视频

这些文件适合放到 CDN 上,因为它们:

  • 内容通常对所有用户都一样
  • 请求量大
  • 体积可能不小
  • 非常适合缓存

所以可以这样理解:

  • npm 像 Maven 仓库,负责"给工程拉依赖"
  • CDN 像"全国分布式静态资源缓存网络",负责"给浏览器加速下载文件"

三、一个实际例子:把公司自研 SDK 发布到 CDN

假设你们公司开发了一个浏览器可直接使用的监控 SDK,产物是:

  • monitor.min.js
  • monitor.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" />

那整个发布过程通常是这样的:

  1. 前端或 CI 把 SDK 打包成浏览器可直接使用的静态文件。
  2. 把这些文件上传到源站。
  3. 在 CDN 厂商控制台配置加速域名和源站地址。
  4. 把 CDN 域名的 DNS 解析指向 CDN 厂商提供的 CNAME。
  5. 业务页面改为引用 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-Control
  • Expires
  • ETag
  • Last-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 时,大致过程是:

  1. 先查到它是一个 CNAME。
  2. 再继续解析 cdn.company.com.vendor-cdn.net
  3. 这个解析过程进入 CDN 厂商自己的 DNS 调度系统。
  4. 厂商根据用户地域、运营商、网络质量、节点负载等因素,返回一个最合适的边缘节点 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 边缘节点
节点是否已有缓存?
直接返回静态资源
回源到源站拉取文件
节点写入缓存

这个图里最重要的点有两个:

  1. CNAME 不会把文件存起来,它只是让请求先进入 CDN 厂商的调度链路。
  2. 真正从源站"拷贝"文件到 CDN 节点的动作,发生在节点缓存未命中之后的回源阶段。

8. 为什么不能直接把域名解析到源站 IP

如果你直接这样配:

text 复制代码
cdn.company.com -> 源站服务器 IP

那请求就会直接到源站,CDN 就根本接不住这个流量,也没法做:

  • 就近调度
  • 边缘缓存
  • 抗带宽压力
  • 热点文件分发

所以从接入关系上看:

  • 配 CNAME 到 CDN 厂商域名,表示"先走 CDN"
  • CDN 节点缓存 miss 后再回源,表示"源站作为最后的权威来源"

这也是为什么我们常说:

源站在 CDN 后面,CDN 在用户前面。

六、最关键的问题:源站的文件是怎么"拷贝"到 CDN 节点上的

这是最值得重点理解的地方。

结论先说

默认情况下,CDN 不是你一发布文件,它就自动立刻复制到全球每一个边缘节点。

更常见的是:

  1. 用户请求某个资源。
  2. 就近的 CDN 节点发现自己没有这个资源。
  3. 这个节点回源到你的源站拉取文件。
  4. 拉取成功后,节点把文件缓存下来。
  5. 后面再有用户访问这个节点时,直接从节点返回。

所以,"源站内容进入 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 分发给工程使用,而浏览器最终加载的 jscss 等静态资源则适合通过 CDN 加速;CDN 节点上的资源通常不是人工逐个上传,而是在缓存未命中时通过回源从源站拉取并缓存,必要时再配合预热减少首访延迟。

如果你已经理解了这件事,那么你对前端里 CDN 的认知就已经很接近生产实践了。