尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

异步加载实战指南:从懒加载到路由分包的首屏提速方案

发布时间:2026/9/29 7:57:47

资讯中心
01
ARTICLE

异步加载实战指南:从懒加载到路由分包的首屏提速方案

异步加载实战指南:从懒加载到路由分包的首屏提速方案
前端圈这两年都在聊性能优化什么首屏加载、白屏时间、秒开率归根结底绕不开一个核心问题用户拿到页面到真正能操作到底等了多久。而异步加载恰恰是我在项目里体会到“性价比最高、收益最明显”的一招。我最早接触这个概念是在做移动端 H5 的时候那时候一个页面动不动几百 KB 打包体积还没上 HTTP2并发连接就 6 个图片、脚本、样式全堆在首屏加载体验只能用“随缘”形容。后来老老实实把异步加载吃透从脚本的 defer/async 到路由懒加载再到图片的懒加载和组件动态导入整套组合拳打下来首屏时间从 5 秒压到了 2 秒出头体感差别非常大。这篇文章想把我在实际项目里总结的异步加载与性能优化经验完整拆一遍包括前端和小程序、桌面端等场景下能直接落地的方案、背后的原理、踩过的坑以及排查工具怎么用。适合正在接触性能优化、想提升页面加载速度、或者准备在面试中聊清楚这套体系的同学参考。1. 异步加载的底层逻辑与设计思路1.1 为什么“异步”能带来性能收益先说一个生活类比。你去餐厅吃饭如果厨师先把所有食材准备齐了再开始做你的菜那高峰期你大概率饿到前胸贴后背。如果厨师一边备料一边炒菜哪里先熟哪里先上你的等待时间就短得多。页面加载也是同样的道理。浏览器加载页面时HTML 解析过程是自上而下的。传统同步脚本有一个非常粗暴的特性脚本被发现时直到执行完HTML 解析都会被阻塞。如果这个脚本很大、很慢或者依赖了慢接口整个页面就会卡在那个位置用户看到的只有白屏。异步加载的核心就是把这个“阻塞”拆掉让关键资源先到非关键资源在后面慢慢跟上。这里面最常见的就是script标签上的defer和async属性。它们让脚本在下载时不再阻塞 HTML 解析区别在于执行时机deferHTML 解析完成后按顺序执行适合依赖 DOM 结构、有先后依赖的脚本。async下载完成后立即执行顺序不保证适合独立、跟页面结构没有关系的小工具脚本。我把这两者理解成“排队进考场”和“先到先做题”defer是排队进考场所有人到齐按顺序坐好再统一开考async是先到先做题谁先到达谁就先开始不用等别人。1.2 请求层面异步设计的取舍除了脚本加载请求层面的异步设计也特别值得提。早期页面经常为一个接口数据开启同步 XHR 请求在主线程上干等这几乎是把白屏时间当玩具踩。后来 Fetch API 和 axios 这类库默认全异步配合 Promise、async/await 处理结果请求一发出就继续做别的事情数据到了再响应式渲染这才是现代应用该有的状态。我做过一个统计报表页面原本 13 个接口是串行请求的全部跑完大概 4.5 秒。后来把所有互不依赖的接口改为并列发起用Promise.all聚合结果整体耗时直接降到 1.8 秒。这里有一个很重要的取舍多个接口并行发并不是越多越好。移动端设备的网络连接数、服务端的压力都要考虑如果一口气发 20 个请求小水管带宽会被挤爆。实际项目的做法通常是按“首屏依赖”和“非首屏依赖”划分首屏必须用的接口并行发起尽快拿到结果。非首屏接口等页面渲染完或者监听用户即将操作到某个区域时再触发。这样既保证了关键内容的快速展示又不会因为无脑并行带来体验上的反效果。1.3 模块化与分包的异步策略模块化开发是本世纪前端最重要的一次升级它把代码拆成了很多小文件但如果不做分包策略打包工具会把所有模块打成一个巨大的 bundle异步加载又从哪来呢所以现代构建工具的异步策略通常结合动态导入dynamic import来实现。Webpack、Vite 或 Rollup 这类工具遇到import()语法时会自动把对应模块单独分到一个 chunk需要时再去服务器下载和执行。这就是路由懒加载、组件按需加载的实现基础。一个一万行代码的大型后台系统如果所有路由的页面全都打成一个包首屏可能要把所有页面的业务代码全部下载完才渲染这显然不合理。做成按路由分包后用户访问哪个页面就下载哪个页面的代码其他页面留到跳转时再加载首屏资源体积可以缩小一半甚至更多。异步加载的核心思路总结起来就一句话把“一次性全部交给用户”改成“先给关键的再给次要的最后给可不用的”。下面几个章节我会把每种方案在实操中的细节拆开讲。2. 核心场景拆解脚本、路由与组件的异步加载方案2.1 经典脚本加载方案对比与使用边界脚本加载是历史最久、也是最容易被忽略的异步优化点。很多老项目里jQuery 时代就习惯把脚本放在/body前面以此来规避脚本阻塞 DOM 解析的问题。这个习惯到今天依然有效但已经不够“优雅”。我总结了一张对比表方便小白直接选用方案加载时机执行时机适用场景放在 body 末尾HTML 解析到末尾时开始下载下载完成后立即执行无依赖的普通脚本兼容性最好deferHTML 解析过程中后台下载文档解析完毕后按顺序执行需要 DOM 的脚本需要执行顺序的脚本asyncHTML 解析过程中后台下载下载完成后立即执行独立统计脚本、埋点脚本、无依赖的小工具动态创建 script任意时机动态插入加载完成后执行需要懒加载的第三方脚本、用户交互时才需要的脚本实际操作中我踩过一个典型坑某个第三方地图 SDK 的加载顺序不能乱它要求先加载核心 SDK 再加载插件 SDK。我当时用了async插件先回来了直接报错。后来把两个标签都改成defer才稳定解决。这类有依赖关系的脚本即使不阻塞 HTML 解析也必须保证执行顺序不能用async。还有一个经验很多团队统计脚本喜欢用async放在head里。这种做法是正确的因为埋点脚本通常独立无依赖并且下载执行得越早越不容易漏掉用户的访问行为。但同时也有一个反直觉的点就是async 脚本下载一旦完成会暂停 HTML 解析去执行如果这个第三方脚本体积特别大执行时间特别长还是会卡一下主线程。所以如果只是简单埋点我更倾向于把轻量统计脚本用async而把重型第三方 SDK 放进动态异步加载的队列里。2.2 路由懒加载与组件按需加载的落地姿势现代单页应用SPA中路由懒加载是异步加载最普遍的应用形式。我用 Vue 或 React 的时候会在路由表定义里直接用动态导入// Vue Router 示例 const routes [ { path: /home, component: () import(../views/Home.vue) }, { path: /user, component: () import(../views/User.vue) } ]// React Router 示例 import { lazy, Suspense } from react const Home lazy(() import(../views/Home)) const User lazy(() import(../views/User)) // 在路由组件外包一层 Suspense打包之后每个路由就是一个独立的 chunk首屏只下载当前路由的代码。这种做法的收益在一个大体量项目中是最直观的我不需要改任何业务代码只需要把所有路由的静态引入变成动态导入首屏资源体积就会立刻下降。组件按需加载要稍微多考虑一些。不是所有组件都适合懒加载我用的判断标准是这个组件是不是首屏一定看得到如果首屏就要渲染懒加载反而会造成闪烁和延迟因为组件代码还在下载中。适合懒加载的组件通常是弹窗类组件打开前才需要真正渲染。折叠面板里的复杂图表用户没展开时根本不需要。管理后台的编辑表单不点击“编辑”按钮就不需要表单逻辑。路由切换后才出现的次级功能区块。实际开发中组件懒加载还需要搭配 UI 界面上的占位处理。如果一个表格下面有一块图表区域需要动态加载我会先在图表位置放一个轻量的骨架屏或者简单的 loading 占位等异步组件加载完成后再替换。这里需要注意的是骨架屏本身不要做得太重否则你又把首屏压力增回了原来的水平。2.3 资源预加载与按需加载的配合异步加载的另一个重要配套是preload和prefetch。它们看起来像是异步的另一种形态但其实思路不同preload提前下载当前页面马上就会用到的关键资源比如首屏背景大图、关键字体、核心 JS 文件。prefetch提前下载用户“接下来很可能访问”的页面资源利用浏览器空闲时间加载。我做过这样一个优化一个官网首页背景图有 2MB用懒加载显然不合适因为首屏就要展示不做预加载会有一段白图时间。后来在首页head中加了一行link relpreload asimage href/images/hero-bg.webp浏览器会更早开始下载这张图片再配合图片压缩转 WebP白图问题顺利解决。prefetch的典型场景是后台系统。用户登录之后访问的第一个页面大概率是“工作台/仪表盘”而“工作台”往往会在侧边栏列出很多下级页面。我可以在工作台加载完后悄悄prefetch几个最常点击的下级路由的 chunk等用户真的点过去时页面几乎是秒开。这比完全懒加载的体验更好。不过要注意prefetch 不能滥用。如果页面有几十个路由全部 prefetch 等于把懒加载省下来的流量全花回去。我一般只挑访问频率最高的 2 到 4 个页面做 prefetch其他的保持懒加载。2.4 图片与媒体资源的异步加载细节图片懒加载可以说是异步加载里最常见、最直观的优化手段。现代浏览器的loadinglazy属性已经足够好用img srcproduct.jpg loadinglazy alt商品图 /这个原生属性不需要额外引入 JavaScript 库浏览器会自动判断图片是否进入可视区域再决定是否发送网络请求。但这里有个细节很多新手不知道原生loadinglazy对“首屏图片”没有意义而且它默认的阈值是 1250px 到 2500px 之间也就是距离当前可视区域还有接近一两屏的高度时就可能开始加载。所以真正首屏的 banner 图、关键商品图不应该懒加载反而应该用 preload 去保证尽早加载。对于更细粒度的图片优化我可以选择 IntersectionObserver 手动控制const images document.querySelectorAll(img[data-src]) const observer new IntersectionObserver((entries) { entries.forEach((entry) { if (entry.isIntersecting) { const img entry.target img.src img.dataset.src observer.unobserve(img) } }) }) images.forEach((img) observer.observe(img))IntersectionObserver 的问题在于兼容性和坑。移动端的低版本 WebView 不一定支持需要做降级方案另外图片加载失败时要有降级处理不能让它静静碎掉。还有一点懒加载必须给img元素设置正确的宽高比否则图片进入可视区域后加载完成布局会突然跳动算是一种布局偏移CLS问题影响性能指标。视频资源的异步加载更需要注意。video默认不会预加载所有资源但如果没有正确设置preload属性浏览器依然可能加载一到几秒的媒体数据。我的做法是默认为视频设置preloadmetadata只加载元信息用户点击播放后再动态设置preloadauto并调用video.play()。这样首屏不会因为大视频消耗流量同时保留了播放的流畅度。3. 构造完整异步加载性能优化链路从监控到落地3.1 性能现状的诊断与关键指标在动手优化之前一定要先量化现状。不使用工具全靠猜性能问题那就是碰运气。我常用的工具组合是浏览器 DevTools 的 Performance 面板、Lighthouse、以及本地抓包或线上监控平台。最关注的指标有三个LCPLargest Contentful Paint最大内容绘制页面视口内最大的可见元素被渲染出来的时间。它直接反映了用户感觉上“页面有没有出来”。FCPFirst Contentful Paint首次内容绘制页面上第一次绘制出内容的时间。TTITime to Interactive可交互时间页面用户能真正完成点击等操作的时间。这几个指标在 DevTools 里都能直接看到。实操建议是打开隐身模式把网络条件调成 Fast 3GCPU 降速 4 倍再跑一轮页面模拟低端手机和弱网下的真实体验。这时候异步加载带来的差异会非常明显。3.2 四步落地的异步加载改造流程我通常用下面四个步骤完成一个页面的异步加载改造可以直接照抄第一步资源大盘点打开 DevTools 的 Network 面板禁用浏览器缓存刷新页面把所有请求记录截图。按资源的类型、体积、加载优先级排序标出前三大的脚本、样式、图片和接口。这个盘点结果决定了优化重点。第二步确定首屏关键资源关键资源指的是页面第一次渲染时缺少它就无法正确显示的资源。视觉上又把它们分成两类首屏视口内能看到的内容和首屏视口外滚动才能看到的内容。首屏以外的内容能异步就异步能懒加载就懒加载。第三步技术改造具体包括把非关键脚本全部加上defer或async。把路由和组件改成动态导入。对图片、视频、iframe 设置懒加载。对第三方 SDK 做动态插入和延迟执行。把体积过大的依赖放到独立分包并在需要时才加载。第四步效果回归与监控改造完成后重新在相同网络环境下跑一轮 Lighthouse对比 LCP 和 TTI 数据。注意必须保持测试环境一致否则数据没有对比意义。之后在线上接入性能监控长期跟踪几个核心指标防止后续迭代把性能回退。3.3 性能优化中必须避开的异步陷阱异步加载不是设置好就一劳永逸有几个陷阱特别容易踩第一个陷阱是异步组件的水合Hydration问题。在 SSR 或服务端渲染项目中如果客户端异步加载组件时机不对可能出现“首屏内容已显示但交互不可用”的尴尬。这类项目的异步加载要和水合策略配合否则用户看到一个能看的页面点按钮却没反应体验远比白屏还糟。第二个陷阱是异步脚本报错遮挡。有些团队在入口处配置了window.onerror把错误打印到全屏遮罩上。如果某个异步组件加载失败就可能导致用户看到一张错误弹层。我的做法是全局监听error事件时对script error这种跨域错误做区分不能一刀切把加载失败的错误直接暴露给用户。第三个陷阱是依赖循环。异步加载会导致模块之间的加载顺序变得不固定如果代码里存在循环依赖在同步环境下往往不会暴露但在异步加载时可能抛 “Cannot access before initialization” 这类错误。排查方法是在构建工具中开启循环依赖检测插件从根上杜绝。第四个陷阱是重复加载。动态导入的组件如果被多个地方引用又没做好缓存可能被加载出多份实例内存白白翻倍。小项目问题不大大型项目要在模块设计阶段避免多个入口重复导入同一个重型组件。3.4 优化前后对比与效果数据参考这里分享一个我实际做过的移动端 H5 项目数据方便你有一个量级上的概念。那是一个商品详情页改造项目项目优化前优化后首屏 JS 体积486 KB86 KB首屏请求数34 个17 个LCPFast 3G 模拟5.6 s2.3 sTTI7.2 s3.1 s图片流量3.1 MB1.2 MB这个结果的核心变化来自路由懒加载把大部分详情页脚本拆出去了主图、价格区文案这些首屏关键资源用 preload评价列表、推荐商品等滚动区域全部改为懒加载视频改成了元数据预加载模式。整个改造大概花了一个星期业务代码改动量很小没有动后端的任何逻辑。你也可以在自己项目里复现这条链路先跑一遍诊断记录数据然后按上面的方法改造异步加载最后再跑一遍诊断把数据体现在团队文档里。性能优化的说服力永远建立在可量化数据上而不是感觉上。4. 常见问题排查与避坑指南4.1 首屏一直白屏异步加载到底生效没生效出现“改造了异步加载首屏还是白屏”的情况优先检查一个点框架入口文件是否还是同步的巨大 bundle。很多项目的懒加载只处理了业务页面但把框架核心、UI 组件库、状态管理、路由配置全都塞进了入口 chunk导致入口 chunk 比之前小不了多少首屏自然降不下来。实操排查方法打开 Network 面板看第一个 JS 资源体积多大。如果超过 200 KB 裸体积就要考虑把组件库改成按需引入把大的第三方库比如图表库、日期库从入口挪进异步组件。另一个常见原因是CSS 全量打入入口样式。JS 懒加载了但 CSS 还是全量一起打包页面渲染依然被阻塞。现代构建工具已经内置了 CSS 提取和代码分割但要检查配置有没有正确开启。我是吃过这个亏的明明 JS 已经拆出去几百 KB样式文件还是 300 KB 全量加载首屏从来就没快过。4.2 懒加载图片优化后反而出现“图片跳动”这是最让新手抓狂的问题。图片懒加载的机制是在图片进入可视区域附近才拉取数据加载完成之前这张图片的高度可能是 0于是页面布局在不断变化下面的内容跟着顶下来用户操作时页面跳来跳去。解决方案非常简单粗暴给每个具体尺寸的图片容器设置占位宽高比。如果你用的是 CSS 的aspect-ratio属性或padding-bottom百分比技巧布局从一开始就是稳定的懒加载加载完只需要替换内容不会引起任何跳动。我在实际项目中会为img标签设置width和height属性浏览器会基于这两个属性自动算好占位比例代码也很简洁img srcplaceholder.svg>const AsyncComponent defineAsyncComponent({ loader: () import(./HeavyComponent.vue), loadingComponent: Loading, delay: 200, timeout: 8000 })这里有一个细节值得提及delay: 200表示等待 200ms 后才显示 loading 组件。如果组件加载很快比如 100ms就不会出现闪一下 loading 又消失的情况体验更自然。timeout则是超时保护避免异步加载失败后页面一直卡在 loading 状态。如果是无框架的原生项目就需要自己在渲染前准备一个占位内容用 Promise 来控制替换时机。这个做法虽然啰嗦但可控性最强适合对项目掌控要求高的情况。4.4 异步加载和浏览器缓存的联动问题异步加载依赖浏览器的缓存机制很重。一个 chunk 文件是否命中缓存直接决定了下一次访问页面时还需要花多少流量。我见过一个团队为了“防止缓存”给所有 JS 文件名加时间戳结果每次发布所有用户都要重新下载全部文件路由懒加载的优势大幅度削弱。正确的做法是使用基于内容哈希的文件名比如chat-8f8e1a5.js。只要文件内容变了哈希就会变文件名自然变内容没变文件名不变浏览器命中缓存访问速度飞快。因为异步加载的 chunk 本来就分散缓存命中率对整体性能的影响会成倍放大。另外要注意 Service Worker 缓存中与异步加载的配合。注册 Service Worker 时尽量先让首屏请求优先不要先把几十个 chunk 全部放进缓存队列否则首屏会被缓存写入任务拖慢。我会把缓存策略拆成“首屏缓存优先”和“路由缓存 lazily”对懒加载的页面 chunk 使用 navigate 到后才预缓存。4.5 针对移动端与低端机的经验技巧异步加载在移动端的表现和桌面端完全不同我在调试时踩到几个坑顺便分享给你弱网下异步请求的并发策略4G 弱网环境下多个小请求并发比一个大请求更致命因为带宽被切碎。我在部分场景会把多个小接口合并请求或者增加服务端的批量接口能力减少请求次数。内存占用异步加载虽然减少了首屏流量但懒加载进来的组件如果没做好销毁清理会累积内存。移动端尤其明显页面长时间使用后会越来越卡。组件卸载时务必清理定时器、事件监听器、IntersectionObserver 实例。低端机的 CPU 瓶颈低端机的解析执行 JS 也慢。异步加载把代码分段后减少了一段长任务但如果每个异步组件本身执行也重交互仍然卡。可以在浏览器性能面板查看长任务Long Task针对耗时超过 50ms 的任务做拆分或 Web Worker 处理。这些经验不一定适用于每个项目建议你在自己的设备上进行性能测试特别是用中低端 Android 手机实测不要只看电脑上的 DevTools 模拟结果。性能优化的最终裁判永远是真实用户手上的体验。5. 工具链与工程化层面的异步加载配合5.1 构建工具代码分割手动分包与控制粒度在做异步加载工程化时一个很容易被忽略的问题就是“拆包粒度”。拆得太细会产生大量小体积 chunk 文件HTTP 请求增多会拖慢速度拆得太粗又失去了异步加载的意义。我用过一个中间策略为每个路由拆一个 chunk再把体积超过 50 KB 的第三方依赖比如图表库、地图库单独拆出来做长期缓存。在 Vite 中手动分包通常靠build.rollupOptions.output.manualChunks在 Webpack 则是optimization.splitChunks。我以 Vite 为例简单展示一下配置// vite.config.js export default { build: { rollupOptions: { output: { manualChunks(id) { if (id.includes(node_modules)) { if (id.includes(echarts)) return vendor-echarts if (id.includes(lodash)) return vendor-lodash return vendor } } } } } }这样打包后echarts、lodash 这类大型第三方库会形成独立 chunk配合 Content Hash 后可以长期缓存用户再次访问时根本不用下载这些大型文件。不过手动分包要谨慎分得太碎会导致页面并行下载多个小文件在高并发弱网环境反而更慢。我的建议是先让构建工具自动按异步导入分包观察实际加载数据再针对体积前几名的依赖手动分包不要一上来就把所有依赖全部拆开。5.2 监控平台上报与性能回归护栏异步加载的优化效果只有被持续监控数据验证才算真正完成。我在团队里用一个简单上报模型把 LCP、FCP、TTI、以及页面可交互前加载的总字节数上报到监控平台按客户端、网络类型、页面路由维度聚合。每次发版后对比指标如果 LCP 中位数或 p90 有明显回退就要立刻排查。具体上报伪代码// 简单示例PerformanceObserver 捕获 LCP const observer new PerformanceObserver((list) { const entries list.getEntries() const lastEntry entries[entries.length - 1] sendBeacon(/api/perf, { lcp: lastEntry.startTime, path: location.pathname, ua: navigator.userAgent }) }) observer.observe({ type: largest-contentful-paint, buffered: true })这样采集到的数据可以直接在趋势图上看到优化效果。如果没有自建监控也可以用开源工具采集上传大家按团队基础设施来选择。5.3 团队协作中异步加载的约束异步加载方案落到团队协作最容易出现的问题是“每个人都在局部优化全局却退步了”。比如 A 同学把弹窗组件改成了动态导入B 同学却在另一个模块里重新静态安装了这个弹窗重复加载问题就出现了。我的做法是在代码评审里加一条硬性规则凡超过 50 KB 的非首屏组件必须提供动态导入的实现方案并标注它所属的模块归属。同时用构建产物的可视化分析工具比如rollup-plugin-visualizer、webpack-bundle-analyzer定期审查整体包组成从源头控制体积膨胀。另外在 README 或者团队文档里写一页“异步加载约定”把哪些资源必须懒加载、哪些资源必须预加载、哪类组件禁止放进入口写清楚。团队工程化的核心是让大家有一个共同的参照系。6. 最后再聊聊我的一些实操体会异步加载与性能优化说到底不是一套固定的招式而是一种“权衡的艺术”。性能指标之间往往互相牵制懒加载减小了首屏流量但可能增加用户操作后的等待preload 提前抢占了带宽却可能让首屏最关键的接口晚返回拆包更细能提升缓存命中但可能增加请求次数。真正吃透这套知识靠的是多在实际项目中做数据对比把每一条策略的代价和收益都摸清楚。我个人的习惯是每做一个性能优化改动都先记录改前数据再记录改后数据最后把结论写成一条团队可复用的 note。慢慢你会发现大部分项目的性能问题不需要多高深的技术异步加载 资源压缩 合理缓存已经能解决七八成问题。剩下的二成才轮到 Web Worker、离屏渲染、内存调优这些进阶方向。如果你现在正好在优化一个访问慢、白屏久或者打包体积很大的项目不妨从“首屏资源清单”开始把所有非首屏的东西全部列出来然后一项一项给它们安排上异步加载方案。这个开头很简单但收益往往比想象的更可观。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。