做 Blazor WASM 项目这些年我对官方的每次改版都是又期待又忐忑。期待是因为 C# 写前端的体验确实独一份忐忑是因为每次发版之后总有几个用户跑来反馈“还是旧数据”“页面白屏”“功能点了没反应”。排查到最后十有八九都是浏览器缓存里那堆旧 dll 在捣乱。说实话Blazor WASM 这几年最劝退人的不是性能而是这个“发版不更新”的老大难。直到最近我把手头一个某跨平台系统 X 从 .NET 8 迁到了 .NET 10才第一次感觉这个历史包袱被官方认真收拾了。这篇就围绕 Blazor WASM 在 .NET 10 里的升级重点——缓存自动清除机制聊聊背后的实现思路、迁移时真正要动手的地方以及我实测下来容易翻车的几个细节。不管你是正准备从旧版本升级还是刚接触 Blazor 想少踩坑这篇文章都值得看完。1. 先看清楚过去那个“用户端不更新”的坑到底在哪里1.1 从 dll 下载运行说起Blazor WASM 应用本质上是一堆 .NET 程序集dll加上运行时文件在浏览器里通过 WebAssembly 执行。用户第一次打开页面浏览器要把这些资源全部拉下来存进缓存之后每次访问都优先用缓存里的东西。问题就出在这里传统 JS 打包工具构建出来的文件名通常带内容 hash比如app.a31f4.js,改一个字节文件名就变了缓存天然失效。而 Blazor WASM 默认发布出来的程序集文件名一般不带这种全量 hash或者 hash 只在清单文件里体现。用户浏览器里的旧 dll 只要还在下次启动时就可能加载到旧代码。我在实际项目里遇到过的典型症状有三种第一种是页面直接白屏console 里报Could not load file or assembly之类的错误第二种是页面能打开但功能还是老逻辑比如后端接口字段都改了前端还在按老字段解析第三种是最迷惑的——首次访问没问题切几个菜单之后突然崩溃原因是某个动态加载的程序集和已缓存的部分版本不一致。1.2 为什么这事特别难排查如果你的应用是纯服务端渲染发版后用户刷新一下基本就能拿到新页面。但 Blazor WASM 的加载链路里index.html、blazor.boot.json、一堆 dll、以及可能的 Service Worker 各自有不同的缓存策略任何一个环节没对齐表现出来的就是玄学 bug。我自己踩坑最深的一次是发布后本地怎么测都正常线上用户却一直走旧版本。后来才发现是部署平台给静态资源自动加了很长的Cache-Control: max-age浏览器直接把整个_framework目录缓存在内存里服务端文件已经换新客户端还在用旧的。那次之后我对静态资源响应头格外敏感。1.3 社区以前的“土办法”其实都是打补丁在没有官方方案之前社区里流传着几类做法我也基本都试过。第一类是在 script 标签上加版本号类似?_v20250101。这个方法对浏览器常规缓存有用但对 Service Worker 里的 Cache Storage 无效因为它根本不看 URL query。而且版本号靠手改或构建脚本注入漏一次就玩完。第二类是让 Service Worker 每次启动都执行caches.keys()然后清空所有缓存。这个做法到手很简单效果也确实立竿见影但代价是每次打开应用都要重新下载全部 dll速度慢不说离线场景也直接废了。第三类是服务端中间件统一给_framework目录加no-cache响应头。这比前面两种靠谱但仍没有解决一个问题浏览器该怎么知道新版本到了以及先清理哪些旧缓存项。本质上大家都只是在“让缓存失效”这个层面打转没有一个机制能从应用自身资源清单的角度去判断“该清什么、该留什么”。2. 核心机制.NET 10 在资源指纹与缓存清理上做了哪三件事2.1 启动阶段多了一次“版本探针”升级到 .NET 10 之后我用 DevTools 的 Network 面板观察应用的加载过程发现启动序列和以前有明显差异。除了常规的blazor.boot.json请求外启动脚本会先带着条件请求头去服务器拿最新版资源清单然后和当前 Cache Storage 里保存的指纹做比对。这个设计说白了就是给整个 WASM 资源集一个总版本号。以前你只知道“我发版了”但浏览器不知道现在应用启动时会主动问服务器“你那边资源还是不是这个版本”服务器只需要返回一个很轻的响应告诉它版本变没变。2.2 检测到版本变化后的处理流程我通过抓包和看 Application 面板把新版本机制下的完整流程整理成了下面几条用户访问入口页面浏览器加载基础 HTML启动脚本向服务器发起对blazor.boot.json的条件请求带If-None-Match或If-Modified-Since服务器返回304 Not Modified说明版本没变继续用缓存如果返回200且内容里的资源 hash 列表有变化启动脚本会直接把 Cache Storage 中属于该应用的旧缓存全部移除再触发一次全新下载下载完成后应用按新版本正常初始化。这套流程最舒服的地方在于它不需要用户手动硬刷新也不需要开发者维护任何版本号。发布新版本后用户下一次打开页面自动就是新版本没发版的时候也不会因为每次强制验证而把缓存全部废掉加载性能没有明显折损。2.3 相比旧版本策略的实质差异为了看得更直观我把旧策略和 .NET 10 的行为做了个对比对比维度旧版本常见行为.NET 10 行为资源请求直接读 Cache Storage不主动校验服务器先做条件请求比对资源指纹版本感知依赖开发者手动加版本参数或改配置启动脚本自动感知服务器端变化缓存清理清空全部或完全不清理精准清空旧程序集缓存保留基础文件发版后用户体感各种白屏、旧逻辑、缓存串版本下一次打开即新版本离线能力要么全清缓存失去离线能力要么长期不更新缓存仍保留版本变化时重建这中间最关键的设计思路是从“尽力而为地清缓存”变成了“从资源清单源头判断一致性”。这个问题一旦从源头解决后面很多维护时的小动作都不需要了。2.4 和 Service Worker 的关系变化如果你之前手动配置过 Service Worker这里要多说一句。旧方案里Service Worker 承担了大量“判断资源新旧”的工作开发者自己要写不少代码来处理缓存版本。而在 .NET 10 的新机制下核心判断逻辑已经内建到应用启动流程里Service Worker 回归到相对单纯的静态资源缓存层角色。我自己迁移时最大的感觉是原来 service worker 里那一堆CACHE_NAME版本管理和清缓存逻辑可以删掉大半只保留网络优先和基础回退就够了。这对维护成本是实打实的降低。3. 动手迁移从 .NET 8/9 升级到 .NET 10 的关键步骤3.1 升级前的工作负载准备迁移的第一步不是改代码而是把本机工具链准备好。用 .NET 10 的 SDK 重新构建 Blazor WASM 项目需要确保 WebAssembly 相关的工作负载已经更新否则编译时会提示找不到目标框架或缺少关键工具链。命令行下执行两条就够了dotnet workload update dotnet workload list我自己习惯在正式迁移前用dotnet new blazor -int WebAssembly --framework net10.0新建一个临时项目先确认本机环境能完整跑通默认模板。这样做可以把“环境问题”和“项目迁移问题”分开排查避免遇到报错时分不清是工具链的锅还是项目配置的锅。如果本机同时装了多个 SDK优先用全局 json 把当前目录固定到 .NET 10避免命令行工具选错版本。这类问题很容易在后续执行dotnet restore时暴露出来。3.2 项目文件里真正要动的点打开项目文件最核心的改动是目标框架。把项目里的TargetFramework改成Project SdkMicrosoft.NET.Sdk.BlazorWebAssembly PropertyGroup TargetFrameworknet10.0/TargetFramework /PropertyGroup /Project这里有一点容易被忽略如果项目里显式引用了Microsoft.AspNetCore.Components.WebAssembly或Microsoft.AspNetCore.Components.WebAssembly.DevServer这类包且版本号是硬编码的迁移后要检查是否需要跟着框架版本一起调整。我在迁移某图像处理 Demo 时就因为package version写死导致反复构建失败去掉显式版本号全部交给框架隐式管理后才恢复正常。另外如果项目之前为了减小体积开了PublishTrimmed或RunAOTCompile迁移后建议重新验证一遍发布结果。.NET 10 对 WebAssembly 的裁剪规则有所调整同样的配置在不同版本下产物体积和运行行为可能会有差异。我见过一个项目开启 AOT 后迁移到新版本反而出现编译错误的情况把配置项临时关掉再逐个开启排错效率会高很多。3.3 Service Worker 文件怎么跟着改默认 Blazor WASM 项目带两个 Service Worker 相关文件service-worker.js开发用和service-worker.published.js发布用。迁移到 .NET 10 后如果你之前的发布版 Service Worker 里写了大量手动清缓存逻辑现在可以大幅简化。我迁移后的service-worker.published.js核心逻辑只保留了网络优先加上基础离线回退self.addEventListener(fetch, (event) { event.respondWith( (async () { const cache await caches.open(blazor-resources); try { const fresh await fetch(event.request); if (event.request.method GET) { await cache.put(event.request, fresh.clone()); } return fresh; } catch (err) { const cached await cache.match(event.request); if (cached) { return cached; } throw err; } })() ); });这段代码的思路是能联网就网络优先请求成功顺手更新缓存网络挂了才回退到缓存。资源新旧判断交给应用启动逻辑去做不再放在 Service Worker 层。这里有个值得注意的细节迁移前如果你已经注册了旧 Service Worker并且它里面有旧的缓存清理逻辑浏览器端的 Service Worker 文件本身是被单独缓存的。升级后要确保新 Service Worker 能正常替换旧版本通常需要在注册代码里保留最基本的skipWaiting和clients.claim处理否则可能出现“新代码配旧 Service Worker”的尴尬状态。3.4 发布配置和静态资源响应头处理说完代码再补一个容易忽略的部署项静态资源响应头。新的自动清理机制依赖启动脚本能及时拿到最新的blazor.boot.json。如果部署平台给这个文件配置了过长的强缓存时间浏览器可能在条件请求阶段就命中了本地缓存拿不到服务器端的新版本。我自己部署时会单独给_framework下的清单类文件做比较保守的缓存策略保证每次启动至少能发起一次有效的版本校验。具体到部署配置里思路是给blazor.boot.json、index.html这类入口资源设置no-cache给 dll 等资源设置较短的max-age。如果用的是静态文件服务器直接在中间件里给固定路径加响应头就可以如果用的是对象存储或者 CDN优先看平台是否支持对特定路径设置独立缓存规则。发布后用浏览器无痕窗口访问一次在 Network 面板里确认blazor.boot.json不是直接命中 memory cache而是实际发出了请求才能判断缓存策略真的生效。4. 验证与排错用 DevTools 把自动清除流程看个明白4.1 验证自动清除是否生效的完整过程升级完不代表万事大吉还得验证新机制真的在工作。我一般按下面三步走第一步发布一版旧代码正常打开页面确认应用可用。然后修改一个明显的 UI 文案重新发布。第二步在同一个浏览器里不要手动清缓存直接刷新页面观察 Network 面板。正常情况下blazor.boot.json会先发出请求服务器返回新版本内容随后能看到_framework底下一批 dll 重新下载。第三步打开 Application 面板进入 Cache Storage检查旧缓存条目是否已经被移除。如果发现旧缓存还在说明启动脚本没有成功触发清理流程需要排查服务器端版本校验环节是否出了问题。我还习惯配合 Console 面板看是否有版本更新相关的日志。开发模式下 .NET 10 的启动脚本会输出更详细的初始化信息能从日志里直观看到“检测到版本变化”的踪迹。4.2 常见问题速查表问题现象可能原因排查思路发版后还是旧版本入口文件被强缓存检查 index.html 和清单的响应头改为 no-cache升级后首次启动白屏旧 Service Worker 缓存冲突取消注册旧 Service Worker清一次站点缓存版本变化时部分资源 404部署时增量覆盖旧文件被删除发布时先全量上传再切流量每次刷新都重复下载全部 dll服务器在条件请求时返回了完整内容确认服务器是否支持 ETag 或 Last-Modified 校验使用对象存储上传后无法自动更新对象存储不支持条件请求改为每次上传时重命名资源目录或通过 CDN 转发4.3 容易踩到的几个细节坑第一个坑是静态资源服务器不支持条件请求。我在做某内部管理系统时用的是某云的对象存储直出结果测试发现每次启动都会触发全量重新下载原因就是对象存储对条件请求支持不完整版本探针永远判定为“有变化”。最后是通过加一层 CDN 转发解决。第二个坑是 Service Worker 没有及时更新。有几次我升级完代码测试浏览器里始终是老行为排查到最后发现是旧 Service Worker 里写死了CACHE_NAME新版本发布文件还没成功替换页面里注册的那一个。这种时候建议在 app 入口处加一个后台静默更新 Service Worker 的逻辑或者开发阶段直接在 DevTools 里勾选 Update on reload。第三个坑是发布流程本身。新机制能保证客户端感知到服务器版本变化但前提是服务器上已经放好了新版本文件。如果你的部署流程是增量上传的旧文件被删除后客户端下载新资源时可能遇到 404。更稳妥的做法是发布时先全量上传到目标路径再执行切换。第四个坑和离线场景有关。自动清除机制的设计重心是“版本一致性”它会在检测到版本变化时清掉旧缓存。如果你的应用强依赖离线可用建议预先评估清楚版本更新时先把新资源下载完再清理还是先清理再用多个并行请求把资源找回来。不同管理策略对应的用户体验差异很大。4.4 给同样在迁移路上的朋友几句提醒如果你的项目规模不大只有一个页面升级时最省事的做法是新建 .NET 10 项目把组件和页面代码拖过去重新组织一下依赖。我做过两个项目的迁移发现这种方式比直接改目标框架要干净得多不会把旧项目里攒了多年的配置垃圾也带进来。如果你的项目已经比较庞大、动态加载的程序集很多迁移时优先关注启动序列和资源加载路径的变化有条件的话在测试环境模拟一次“旧版本缓存 新版本发布”的场景确保自动清理不会破坏动态加载的依赖关系。还有一点别把自动清除缓存当成性能优化的解药。应用体积该裁剪还是要裁剪资源该压缩还是要压缩。缓存清理机制解决的是“版本新鲜度”问题而不是“首屏加载太慢”的问题两者不是一回事。我个人在整个迁移过程中的体会是这次官方的方向终于对了——不再让开发者拿 Service Worker 和响应头去拼一套脆弱的缓存方案而是从应用自身的资源版本管理入手把该做的判断内建到启动流程里。你在 DevTools 里能清楚看到它每一步在做什么这比以前的“黑盒缓存”体验要好太多。如果你正准备升级建议先拿一个小项目试水跑通之后再动核心业务系统。升级过程中如果遇到什么奇怪的缓存现象优先按表格里的方向排查八成都是部署链路的问题而不是 .NET 10 本身的问题。