
很多人以为性能优化就是把文件压缩小一点、图片转成WebP、再挂个CDN就完事了。这些当然都是正经手段但我在实际排查过的项目里真正让首屏卡住、启动变慢的十个里有六七个是资源加载的方式出了问题。明明该异步加载的资源被同步阻塞该后置延时的资源抢占在关键路径上该并发的请求排成了串行。异步加载与性能优化这件事本质上是在回答一个问题资源有限的情况下怎么安排谁先来、谁后到、谁滚去后面等。这篇文章是原理篇我会先把异步加载为什么快、底层是怎么运转的讲透再落到Web、移动端、游戏这些场景里给可直接复用的方案。适合正在做首屏优化、启动耗时优化、加载体验优化的开发者也适合被代码不多但页面怎么这么卡折磨的人。配合我看瀑布图、排查异步坑的经验这篇应该是能让你少走不少弯路。1. 为什么加载方式能决定性能上限先建立一个大前提性能问题分两类一类是东西太大一类是东西来得不是时候。前者靠压缩、裁剪、缓存去解决后者就要靠异步加载这类调度手段。很多团队只盯着第一类猛做忽略了第二类结果做了半天用户感知不明显。资源来得不是时候最典型的表现是首屏渲染必须要用的CSS还没到反而是一堆统计脚本、轮播组件脚本先占满了连接主线程本来该做关键渲染却在傻等一个第三方SDK的同步加载。这些问题的根源是加载顺序和优先级没有管理好。1.1 一次加载请求里时间到底花在哪拆开一次资源请求看大致要经过DNS解析、建立TCP连接、TLS握手、发送请求、服务端处理、响应传输、浏览器解析执行。其中真正属于我们代码控制范围的只有最后那一小段解析执行。网络传输和服务端等待的时间往往远超执行时间。举个例子一个200KB的脚本在4G网络下传输可能要大几百毫秒但执行它可能只要几十毫秒。如果你用同步方式加载页面就得等完整个网络链路才能继续渲染——也就是白白等掉了那几百毫秒。所以同步加载真正的浪费不在执行而在等待。异步加载解决的就是把这部分被迫等待从主流程里剥离出去。1.2 同步加载的代价主线程一卡整个页面都等你浏览器的解析、执行、样式计算、绘制都跑在同一个主线程上同一时刻只能干一件事。当主线程在等待同步脚本下载时它什么都做不了用户看到的就是白屏、点击无响应。移动端网络不稳定一个慢请求卡住主线程三五秒的情况太常见了。我用餐厅上菜来类比主线程就是后厨唯一的大厨同步加载相当于大厨放下炒勺亲自跑到门口等食材送到到了才回厨房继续做菜。店里明明还有别的菜能做整个后厨就因为这个等待食材的动作停摆了。异步加载则是让跑腿的去盯食材大厨继续做手头的菜食材到了再通知他处理。这不只是个风格问题是实打实的效率问题。1.3 异步加载的底层逻辑把等待变成调度异步加载并不改变单个资源的加载速度它改变的是资源的加载时机和方式核心就三个动作延后不紧急的资源、并发加载独立的资源、优先加载关键资源。放到具体场景就是首屏不依赖的JS延后到空闲时再下载互不依赖的资源并发请求不排队每个资源按是否影响首屏、是否影响交互分配优先级。这套逻辑Web、移动端、游戏都通用理解了这三个动作后面那些具体方案——async、defer、懒加载、分帧加载、线程池都是在实现它们。提示判断一个资源能不能异步最直接的标准是它是不是当前主流程立刻就要用。不是就延后或异步是就想办法内联、精简、提前预取别一棍子打成异步。2. 异步加载的运行原理事件循环与底层分工异步加载迷惑人的地方在于反直觉JS明明单线程怎么能一边下载一边执行要真正理解就得看事件循环和浏览器底层分工。2.1 单线程模型下异步是怎么同时发生的JavaScript确实跑在单线程上但异步不是同一时刻做两件事而是把任务切分让等待期间CPU先干别的。当你发起一个网络请求真正发请求的动作交给浏览器网络栈去执行这不是JS线程。JS线程发完请求就继续往下跑直到请求结束底层通过回调把结果塞回事件循环JS线程再处理。所以异步的本质是分工负责计算的是JS线程负责等待的是系统底层。两边通过事件循环协作。这也是异步IO场景下程序看起来同时做多件事的原因——实际是等待时间被拿去执行别的任务了。同一个道理移动端把IO初始化丢到子线程Julia这类计算语言把并行任务调度到多线程全都是分工哲学的延伸。2.2 宏任务与微任务执行顺序的坑比想象中多事件循环的经典规则是每轮先执行一个宏任务然后清空所有微任务再进下一轮。宏任务有setTimeout、setInterval、I/O回调微任务有Promise.then、MutationObserver、queueMicrotask。这个顺序在性能优化里有实打实的影响。我处理过一个线上case团队用Promise链做资源加载调度结果页面反而更卡。查到最后是微任务队列里积压了大量Promise回调主线程在一轮事件循环里拼命清微任务迟迟进不了渲染步骤。异步加载不是无脑异步任务的粒度必须控制。延后高耗时逻辑用宏任务而不是微任务微任务里只放必要的状态更新和有依赖关系的操作避免微任务风暴。2.3 真正干活的不是异步这两个字很多初学者把async/await、Promise当成异步的全部其实那只是语言层表达。真正高性能的异步加载靠的是底层IO。浏览器的网络连接池、Android的线程池、Julia的并行调度这些底层的执行者才是关键。理解这点对选型很有帮助如果你的异步加载只是把逻辑包进Promise但底层还是同步执行那它不会带来任何性能收益反而增加复杂度。异步必须对应真实的等待外置——网络请求交给网络栈、IO交给系统、重计算交给独立线程主线程才能腾出来干正事。3. 性能优化核心原理关键路径、优先级与资源竞争异步加载解决阻塞的问题但性能优化还要回答先做什么、后做什么。这就涉及关键渲染路径和资源优先级这套思路在所有端都成立。3.1 关键渲染路径从URL到首屏每一步都是机会浏览器从拿到HTML到画出首屏要经历构建DOM、解析CSS、执行JS、计算样式、布局、绘制。这条链路里的每个环节被拖慢都会直接反映为首屏变慢。关键渲染路径优化有两个核心思路减少关键路径上的资源数量减少关键路径上每个资源的大小。异步加载在这里的作用是挪位置把非关键资源从关键路径上挪走。页面有10个脚本只有2个影响首屏剩下8个是埋点、统计、轮播组件——正确做法是8个全部异步或延后关键路径只留那2个。很多项目反着做所有脚本全都同步放head里首屏被一堆无关脚本拖垮。性能优化的第一刀永远是先理清关键路径上的资源清单。3.2 资源优先级与带宽竞争有限的带宽怎么分HTTP/1.1时代浏览器对同一域名的并发连接数有限一般6个左右多出的请求排队。HTTP/2通过多路复用解决了并发传输但总带宽有限这个物理约束没变。首屏要用的CSS和一个统计脚本同时抢带宽统计脚本占用了连接首屏就慢了。解决方案是给资源排优先级。浏览器自己有资源优先级机制开发者也能用preload、preconnect主动影响加载顺序。移动端和游戏更激进因为它们不只考虑网络还考虑内存。一次加载太多资源到内存系统可能触发回收导致卡顿。手游常用的分帧加载、按场景加载、对象池都是这个思路的产物别把东西一口气全塞给系统。3.3 预加载、懒加载、并发加载到底怎么选三个词经常一起出现很多人分不清。我的判断标准很简单预加载资源当前不需要但接下来很快会用。比如首屏渲染完成后预取下一页数据。提前准备但控制量别抢首屏带宽。懒加载当前不需要等需要的那一刻再加载。典型是图片进视口才加载、路由切换才请求对应组件代码。目的是减少初始加载量。并发加载多个互不依赖的资源同时请求。目的是缩短总等待时间。三者不互斥成熟方案都是组合使用关键资源预加载非关键资源懒加载独立资源并发加载配优先级控制。我见过最优的一版加载方案是一张二维表格——横轴是是否首屏需要纵轴是是否依赖其他资源每个资源都对应一个明确的加载策略。4. 落地实操Web、移动端、游戏场景的具体方案原理讲完上点能直接抄作业的。我按三个场景分别讲每个都给到判断标准。4.1 Web场景script的async和defer千万别用反前端异步加载最基础也最容易错的是script标签的三个加载方式。它们的本质区别可以总结成一张表加载方式下载时机执行时机执行顺序普通脚本遇到就下载下载完立即执行阻塞解析按文档顺序defer与HTML解析并行文档解析完、DOMContentLoaded前按文档顺序async与HTML解析并行下载完立即执行不保证顺序我的选型原则业务逻辑、有依赖关系的脚本用defer第三方统计、无依赖的功能脚本用async。原因在于defer保证执行顺序适合模块之间的依赖关系async适合下载完就能跑、不依赖任何人的独立功能。如果把有依赖关系的脚本设async很可能出现库还没执行、业务代码已经在跑的崩溃。再补一个很多人忽略的细节defer脚本在DOMContentLoaded之前执行所以需要DOM解析完成后立即初始化的逻辑选defer是正解。async脚本执行时机不可控别在里面操作还没解析完的DOM。图片的话首屏内的图片别用loadinglazy那会影响首屏绘制首屏外的图片加上lazy是真香。4.2 移动端场景启动优化的异步初始化移动端启动优化盯的核心指标是点图标到首帧画面的时间。这段时间主线程要完成进程启动、组件初始化、布局、首帧绘制一大堆事。最常见的优化手段是异步初始化把日志、推送、统计SDK这类不影响首帧的组件丢到后台线程或者延后到首帧之后。但异步初始化有个大坑——时序依赖。A组件初始化依赖B组件你把A异步化了却没处理依赖运行到一半A被调用才发现B还没初始化直接崩。我的做法是分层核心依赖链上的初始化留在主线程按顺序走非核心独立组件走异步。绝不一刀切全量异步。另一个经验是线程池控制并发度别无限开线程移动端线程太多上下文切换开销比省下的还多。4.3 游戏与计算场景分帧加载与内存换时间手游和重型应用的加载同时面对加载耗时、内存占用、掉帧三个约束。这里的异步加载通常指分帧加载——把大资源包拆成小块每帧只加载一部分避免单帧卡顿。一帧超过16.6毫秒就掉帧一次性加载一个大地图很可能某帧要处理几十毫秒玩家立刻感觉到卡。分帧加载让每帧工作量可控本质是用时间换流畅。Julia这类计算密集场景的性能优化同理想通通过异步任务调度把并行计算拆到多线程避免单线程被长任务占满。内存管理上更要注意异步加载进来的数据用完要及时释放不然内存持续上涨最终拖垮整体性能。这和Web端的资源释放、事件解绑是一个道理——异步加载不只是把东西拿进来还包含用完放回去。5. 常见性能问题排查与避坑记录方案都懂真到排查现场还是容易懵。这部分是我反复踩过的坑比官方文档直白。5.1 瀑布图怎么定位真凶三个指标速查无论Web还是移动端性能工具基本都有资源加载瀑布图。我的排查顺序固定三条先看每个请求的等待时间TTFB判断是不是服务端响应慢再看请求的并发排队情况判断是不是连接数或优先级问题最后看主线程繁忙区间判断是不是执行阶段卡顿。三个指标对应三类问题TTFB长服务端或网络链路有问题加缓存CDN意义不大请求排队说明连接数限制或优先级没排好考虑提升并发、合并请求主线程繁忙说明问题在执行不在加载要优化代码复杂度或拆分长任务。很多团队只盯着总时长看但真正影响用户感知的是关键路径上的总时长。一个统计脚本加载3秒但它异步延后在首屏之后对用户毫无影响一个首屏CSS加载100毫秒慢一点用户都能看到白屏。5.2 异步加载引来的经典坑时序、报错与静默失败异步加载最大的副产物是不确定性你无法保证两个异步任务谁先完成也无法保证某个异步资源一定加载成功。由此会引出几类经典问题时序错乱依赖某个异步资源的代码提前执行报is not a function。错误处理缺失异步资源加载失败静默失败用户看到功能凭空消失。数据上报丢失异步埋点在页面关闭前没发出去统计失真。对策上依赖关系用清晰的加载队列管理别赌时间差不多能赶上所有异步资源必须配失败回退或重试关键数据上报尽量走可靠的发送通道。这些事看着琐碎却是异步化之后必补的课。我做过一个项目启动时间优化了30%但上线后crash率涨了查下来就是异步初始化打乱了原有的依赖时序——优化性能的同时把系统的确定性弄丢了。5.3 几条实测心得可