尧图精选

异步加载与性能优化:从事件循环到关键渲染路径的底层原理与实践

🕒 发布时间:2026/10/1 6:15:15 📁 来源:尧图网络
性能优化的本质从来不是让单个任务跑得更快而是让任务之间的拥堵变少。前端圈聊了这么多年性能真正决定用户体感的往往是加载顺序和执行时机。异步加载这个手段看着基础但把它吃透并用对地方的人并不多。这篇文章我想从原理层面把异步加载和性能优化的关系彻底拆开不讲虚的只聊底层逻辑和可落地的做法。1. 性能差到底差在哪重新认识页面加载的瓶颈很多人一提到性能优化下意识就想压缩代码、合并请求、搞CDN缓存。这些手段有效但都是治标。真正的性能瓶颈通常藏在一个容易被忽略的地方——任务的先后关系限制。页面打开时浏览器要做的事情远比你想象的多下载HTML、解析DOM、下载CSS和JS、执行脚本、渲染像素、处理用户交互。这些步骤之间有严格的依赖关系某个环节卡住后面的全部排队等待。以移动端为例我做过一个统计实验。用一个中型Web应用分别跑3G网络模拟慢速环境和5G网络模拟快速环境在无缓存条件下记录关键时间点。结果很有意思5G环境下网络下载时间占比只有20%左右但脚本执行和渲染时间占了接近一半3G环境下网络等待时间飙升到60%以上执行时间和渲染时间反而相对缩小。这说明什么不同网络环境下优化的重心完全不同。弱网下你要减少请求次数和资源大小强网下你要关注主线程的执行压力。移动端性能优化里有一个经典概念叫“关键渲染路径”Critical Rendering Path。从服务器返回HTML到屏幕上出现像素中间有五个环节构建DOM树、构建CSSOM树、合并生成渲染树、布局计算、绘制。任何阻塞渲染的资源比如同步加载的大脚本、未内联的样式表都会把这条路径拉长。大多数性能问题本质上都是关键渲染路径上的某个环节被无谓地加长了。再往深了说这里还牵涉到一个核心资源——主线程。浏览器的主线程要同时负责解析HTML、执行JavaScript、计算样式、布局、绘制。任何一个长期占用的任务都会让其他任务排队用户感受到的就是卡顿、白屏、点击没反应。异步加载的核心价值就是把非关键任务从主线程的关键时间窗里挪出去让首屏渲染路径上的任务优先执行。理解了这个底层逻辑后续的优化手段就都有了方向感哪个任务先执行哪个任务后执行哪个任务干脆放到另一个线程去跑。下面我会从浏览器原理层面把“异步”这件事彻底讲清楚。2. 异步加载的底层原理事件循环、任务队列与多线程的真面目2.1 事件循环机制JS为什么能“边等边干活”JavaScript是一门单线程语言这在设计之初就决定了。但单线程不代表只能一次做一件事它依靠事件循环Event Loop机制实现了异步的假象。这个机制可以用一个生活场景来理解你去餐厅点餐服务员不需要站在厨房门口等着菜做好再服务下一桌客人她只需要把菜单递进去然后继续接待其他客人厨房做好菜会通过传菜窗口通知她。浏览器的事件循环与此完全一致。主线程执行同步代码遇到异步操作比如网络请求、定时器、事件回调时不会傻等结果返回而是把这些任务交给对应的模块去处理自己继续执行后面的代码。当异步任务有了结果对应的回调函数会被放入任务队列等主线程当前的同步代码执行完毕再从队列里取出来执行。这套机制让JavaScript可以在等待网络响应的同时还能响应用户操作、渲染页面。任务队列还有更细的划分宏任务MacroTask如setTimeout、setInterval、I/O事件回调、渲染事件和微任务MicroTask如Promise.then、MutationObserver。它们执行顺序的规则是主线程执行完一段代码后先看微任务队列把里面所有微任务清空才轮到下一个宏任务。微任务的优先级高于宏任务这点在实际开发中很重要。比如你用Promise去加载资源回调的执行时机就会比setTimeout回调早一拍。理解事件循环机制后异步加载的原理就清晰了把耗时的I/O操作交给系统或底层模块去跑主线程不被阻塞等结果回来后再通过回调或Promise继续处理。网络请求天然适合异步因为等待网络响应的时间可能长达几秒扔给底层网络栈去处理主线程完全不用干等。2.2 防阻塞的关键同步加载为何会成为性能杀手要理解异步加载为什么能提升性能最好的对比就是看同步加载做了什么。默认情况下HTML里通过script标签引用的外部JS是同步加载的浏览器碰到这类标签时会立即停止HTML解析发请求去下载这个脚本下载完再执行执行完了才继续解析后面的HTML。这个行为被称为“解析器阻塞”Parser Blocking。为什么浏览器要这么做因为在JS代码里可能包含影响后续文档解析操作比如document.write直接写入HTML内容。如果浏览器不暂停解析后面解析的内容可能会和脚本插入的内容冲突。这是安全方面的考虑代价就是同步脚本会严重拖慢页面解析速度。我踩过一个特别典型的坑。某个H5活动页底部引入了一个统计脚本和一个聊天组件脚本没加任何属性写在body末尾。结果在弱网下测试时发现首屏时间被硬生生拖长了2到3秒。原因就是聊天组件脚本体积有500多KB而它挂了同步脚本的属性阻塞了后续文档解析。后来改成异步方式引入把统计脚本用标准方式延后处理聊天组件则改成能在用户交互时机再加载首屏性能提升非常明显体感就是页面一下子“跳”出来了。这里补充一个在文档中常被忽略的细节不仅是JS会阻塞解析放在head里的CSS同样有阻塞渲染的问题。浏览器要等CSSOM构建完成后才能渲染页面这是为了避免样式闪烁。有一段时间流行的“同构样式内联”做法本质就是把这个阻塞前置让样式更早到达浏览器。所以性能优化从来不是单一手段的功劳而是全链路任务排队的统筹调度。3. 加载策略的决策树defer、async与运行时动态加载怎么选3.1 script标签的三种行为模式对比处理脚本加载前端有三板斧普通同步加载、defer属性、async属性。很多人知道defer和async都能让脚本异步加载但它们的执行时机差异直接决定了使用场景这里我来拆透。先说普通同步加载遇到即下载下载完立即执行执行完才继续解析文档。其次是defer它的行为是“下载异步执行延后”。带defer的脚本会在文档解析完成后、DOMContentLoaded事件触发前按顺序执行多个defer脚本保证相对顺序。然后是async行为是“下载异步执行随时”。下载完成后立即执行不等待文档解析完成多个async脚本之间也不保证顺序。行为属性下载是否阻塞解析执行时机多脚本顺序无属性同步会阻塞下载完成后立即执行按出现顺序defer不阻塞文档解析完成后执行按出现顺序async不阻塞下载完成后立即执行时机不定不保证并行顺序选择逻辑其实很清楚。需要依赖其他脚本执行结果的、对执行顺序敏感的代码用defer。互相独立、没依赖关系的代码比如第三方统计、监控上报用async谁先下载完谁先执行。一个经典的搭配是页面主逻辑脚本用defer保证页面结构完整后再执行广告、埋点脚本用async它们的加载执行时间轴完全独立。移动端性能优化里常说的“首屏脚本瘦身”就是把这些执行时机不同的脚本分门别类把关键的留给关键时机把不关键的全扔到空闲时间窗里去。3.2 运行时按需加载真正意义上的零阻塞静态的defer和async有一个共同局限脚本无论如何都会在页面生命周期里被下载和执行只是时机早晚的区别。但有些代码模块用户可能根本不会用到——比如一个只在用户点击“打开高级设置”时才需要弹窗组件比如图片查看器、评论插件。把这类资源在初始加载时就下载执行是对网络带宽和主线程的双重浪费。运行时按需加载的思路是把脚本加载的决策推迟到真正需要薄时刻。实现方式多样动态创建script标签插到文档里或者用import()函数返回Promise按需加载ES模块或者通过动态加载器如RequireJS、SystemJS管理模块依赖。下面是一个用import()实现按需加载的简单例子// 比如在用户点击弹窗时才加载对应组件 async function openAdvancedPanel() { // 普通写法会在这个文件里静态引入组件代码导致首屏加载体积膨胀 // 换成动态 import 后这段代码会被单独分包首次加载完全不触碰它 const { AdvancedPanel } await import(./components/AdvancedPanel.js); const panel new AdvancedPanel(); panel.render(); }Webpack、Rollup、Vite这些构建工具遇到动态import语法后会自动做代码分割生成独立的chunk文件浏览器只有在执行到对应import时才会发起请求。这样主包的体积就显著缩减首屏的下载量和解析压力同步下降。实践中我会用这样一个判断标准来决定是否按需加载这个模块在当前页面里有多少用户会立刻用到如果不到30%就值得按需加载如果超过70%按需加载收益不大反而多一次网络请求的开销不如直接打进主包。4. 资源加载层面的异步优化实战4.1 图片懒加载先把真正可见的内容交给网络图片是页面体量的大头尤其是电商类、内容流类页面。优化图片加载最有效的常规操作就是懒加载核心思想是只加载用户当前视口内需要显示的图片视口外的图片暂时不请求资源等用户滚动到附近时才加载。原生实现早已不是问题。给img标签加上loadinglazy属性浏览器原生就支持视口内延迟加载img srcthumbnail.jpg>const observer new IntersectionObserver((entries) { entries.forEach(entry { if (entry.isIntersecting) { const img entry.target; const realSrc img.dataset.src; if (realSrc) { img.src realSrc; img.removeAttribute(data-src); } observer.unobserve(img); } }); }, { rootMargin: 200px 0px, // 提前200px开始加载 }); document.querySelectorAll(img[data-src]).forEach(img observer.observe(img));rootMargin这个参数正是性能优化里“提前量”的体现。你可能觉得图片还没进视口就加载它有点浪费资源但对用户体验而言滚动到某张图片时它已经加载完成或正在加载最后一段远比眼睁睁看着空白占位符出现要好。移动端操作区域小用户滚动速度快这个提前量的价值被无限放大。一般我习惯设200-300px图片体积大或网速偏慢的环境可以再加大。4.2 路由懒加载与代码分包首屏包的减法单页应用SPA项目的性能大头几乎都集中在JavaScript打包体积上。一个常见的React或Vue首屏包动辄几百KB压缩后也得一两百KB再加上解析执行的成本首屏慢是必然的。优化方向很简单让每个路由对应的页面代码独立打包用户访问哪个路由才加载哪份代码。React配合Vite的方面是一个典型示例// React Router 路由懒加载 import { lazy, Suspense } from react; const ProductList lazy(() import(./pages/ProductList)); const ProductDetail lazy(() import(./pages/ProductDetail)); // 路由配置里不再直接引入组件而是用 lazy 包装 const routes [ { path: /products, element: ProductList / }, { path: /product/:id, element: ProductDetail / }, ];Vue Router中做同样的事用的是路由级异步组件// Vue Router 路由懒加载 const ProductDetail () import(./views/ProductDetail.vue); const routes [ { path: /products/:id, component: ProductDetail, }, ];原理都是把组件代码从主包里摘出去独立成一个chunk文件。构建后的dist目录里除了主入口文件还有几十个page级别的chunk文件页面访问时才动态挂载。做完代码拆分后的构建产物体积变化值得观察主包干净了各个页面包变多但互不干扰。这也解决了SPA一个长期痛点——用户明明只是看了一眼首页却要把整个应用的代码全都下载并执行一遍。拆包粒度的把握也很关键。拆得太细比如每个组件都单独拆包会导致页面切换时频繁发起新的请求弱网环境下反而变得迟钝。拆得太粗又达不到首屏瘦身的目标。我的习惯是按路由一级做拆分在这个基础上把体积超过50KB的第三方库单独抽包。多一次网络请求换来的是超长缓存命中和启动路径精简这是值得的。4.3 异步数据请求策略并行、竞态与接口依赖前端性能优化的细节永远离不开数据请求这环。异步数据请求写得好页面等数据的时间就短写得差明明接口响应很快页面还是空着等半天。优化的核心有两个请求并行度和接口依赖顺序。很多页面页头、主体、推荐位的数据来自不同接口。如果把它们一个个串行加载假设每个接口200ms三个就是600ms再加上等待渲染的时间用户看到页面的时间就被拉得很长。正确做法是启动页面时同时发起这些请求然后等所有数据到齐后再一次性渲染或者按数据的整齐程度做分片渲染。Promise.all就是为这种场景准备的async function initPage() { const [bannerData, productData, commentData] await Promise.all([ fetch(/api/banner), fetch(/api/products), fetch(/api/comments), ]); renderBanner(bannerData.data); renderProducts(productData.data); renderComments(commentData.data); }另一个常被忽视的问题和竞态有关。异步接口的返回顺序不受控制如果用户在页面上快速切换筛选条件前一次请求的结果可能比后一次晚返回此时如果直接把数据渲染到页面上页面就会显示过期内容。这个问题几乎每个接触异步加载的同行都踩过。解决方案是给请求加版本标记或序列号只处理最新一次请求的结果let requestId 0; async function fetchProducts(filter) { const currentRequestId requestId; const response await fetch(/api/products?filter${filter}); if (currentRequestId requestId) { renderProducts(await response.json()); } }网络请求本身也是异步任务在移动端优化里它占据了“等待时间”的大头。这节的异步加载本质思路在于如何把用户感知的等待压缩到最小——该同时进行的不要排队无效的返回结果直接丢弃。想通了这一点请求层的优化基本就掌握了七成。5. 主线程降载从异步加载到计算分摊5.1 Web Worker真正意义上的“多线程”方案如果说异步加载解决的是“任务等待”和“资源加载时机”的问题那么还有一类问题是它无法解决的——主线程上的重计算本身。比如对一大段数据进行格式转换、图表库渲染大量数据点、图片解码缩放这些任务一旦触发就会长时间占用主线程页面在此期间无法响应滚动和点击。前面讲的异步加载手段在它面前失效因为这不是排队问题是干活的人太累的问题。Web Worker就是在纯前端语境下唯一能真正把任务挪出主线程的方案。Worker运行在独立的全局上下文里占用独立线程拥有自己的事件循环主线程通过postMessage向它发送数据它也通过postMessage返回计算结果。两者之间不共享内存靠结构化克隆传递数据。一个典型的场景是把大型数据处理交给Worker做。比如在移动端对一段音频数据做音量分析、提取波形峰值这类计算在主线程上会让页面卡住半秒以上放到Worker里则完全无感// 主线程侧 const worker new Worker(/js/audio-worker.js); worker.postMessage({ type: analyze, buffer: audioBuffer, }); worker.onmessage (event) { const { peaks, duration } event.data; renderWaveform(peaks, duration); }; // audio-worker.js 内部 self.onmessage (event) { if (event.data.type analyze) { const peaks calculatePeaks(event.data.buffer); self.postMessage({ peaks, duration }); } }; function calculatePeaks(buffer) { const channelData buffer.getChannelData(0); const peekSize 2000; const blockSize Math.floor(channelData.length / peekSize); const peaks new Uint8Array(peekSize); for (let i 0; i peekSize; i) { let max 0; for (let j 0; j blockSize; j) { const value Math.abs(channelData[i * blockSize j]); if (value max) max value; } peaks[i] max * 255; } return peaks; }用上Worker之后这类重计算就从“交互卡顿几秒”变成了“后台运行几秒”期间的滚动和点击都畅通无阻。比较值得注意的坑是Worker内不能访问DOM也不能调用window上的方法只能在postMessage传数据所以使用前要想好数据序列化方案。大数据量的传递本身也会有性能损耗好在ArrayBuffer这类二进制数据可以用Transferable Objects的方式转移把所有权移交给Worker而不用复制成本极低。5.2 长任务的拆分与调度时间分片与优先级策略如果任务不能挪出主线程比如必须操作DOM或者Worker建造成本太高那还有一种思路把长任务切成小片分多次执行。浏览器在人机交互过程中有一个概念叫“可中断的时间片”每次主线程执行一个宏任务JavaScript代码的执行会贯穿整个任务中间不会让位。一个100ms的长任务在用户眼里就是一个卡顿如果用requestIdleCallback或setTimeout把任务拆成持续10ms的小片浏览器就能在每个片段之间插入渲染和交互响应。这在性能优化领域被称为“时间分片”或者说瓶颈打散。React的并发渲染机制和useTransition就是把这个理念推到了极致允许渲染过程被更高优先级的交互任务打断。原生写法中我们可以简单地用请求空闲调度function processLargeList(items) { const chunkSize 20; let index 0; function processChunk() { const end Math.min(index chunkSize, items.length); for (let i index; i end; i) { // 处理每个条目这里以 DOM 渲染为例 renderItem(items[i]); } index end; if (index items.length) { requestIdleCallback(processChunk); } } processChunk(); }要注意的是requestIdleCallback在不同环境的支持度和触发时机并不一致移动端低版本浏览器上可能需要降级回setTimeout改用双阈值节流避免太密集。设计目标是一致的让主线程上的每一次连续占用时间都被控制在浏览器能容忍的阈值内给交互和渲染留出空隙。实际项目里我很少单独用某一种手段解决所有问题。通常的组合是数据请求全部异步并行切分等待时间路由代码按需加载减小首屏体积重计算放进Worker拿掉主线程的大块占用必要的长渲染操作再切成时间片保持响应。四层降载方案下来页面在低端安卓机上也能保持流畅的滚动和点击响应这在移动端性能优化里比任何花哨的技巧都重要。6. 性能优化的衡量与验证从“感觉变快了”到“确实变快了”6.1 关键性能指标到底该测什么做完异步加载的改造后面临一个现实问题怎么证明优化有效很多同行在优化后凭体感判断“好像快了一点”这在技术复盘里是不够严谨的。围绕移动端性能优化业界已经沉淀了一批关键指标围绕异步加载的改动主要关注四组数值指标含义异步加载优化后的预期变化FCPFirst Contentful Paint首次内容绘制时间用户看到第一个有效内容预期显著缩短LCPLargest Contentful Paint最大内容绘制时间主体内容可见屏主要资源提前加载后缩短TBTTotal Blocking Time主线程长任务导致的阻塞总时长主线程任务打散后下降INPInteraction to Next Paint交互到下次绘制的响应延迟长任务减少后明显降低其中LCP在移动端优化中的权重尤其高搜索引擎也把它作为用户体验的核心参考值。我一般会盯LCP的P75值75分位意思是75%的用户LCP值要低于某个经验阈值比如良好体验可以按2.5秒左右作为观察窗口。只要总体趋势下降、P75逼近目标值就说明改动对用户体感的改善是真实存在的。异步加载改造影响最大的是首屏时间维度的指标。比如图片懒加载配合路由按需加载后LCP大概率快速回落。但这也不是没有代价的——上面这些改动可能让TTFBTime To First Byte服务端响应首字节的时间维持不变甚至因为分包请求变多而略微上涨但后者影响的主要是LCP/INP权衡下来收益明显更大。这也是为什么围绕性能优化的前后对比我建议至少同时记录5到6个指标而不是单看某一个。6.2 实测工具箱Lighthouse、Performance面板与移动端真机调测工具层面我日常的固定搭配是Lighthouse加浏览器Performance面板。Lighthouse适合快速跑分和发现结构性短板它生成分项报告——性能分、可访问性、SEO等对性能分影响最大的因素基本集中在首屏加载路径上直接对应异步加载改动的效果。Performance面板适合精确定位卡顿点录制一段加载过程后查看主线程的火焰图可以清晰看到哪些长任务占用了时间哪个网络请求拖慢了关键路径。但这两个工具都基于桌面环境。移动端性能优化的特殊性在于硬件差异巨大——低端安卓机的CPU性能只有旗舰机的三分之一同样的代码跑出来的性能特征完全不一样。所以有条件的情况下我都会用真机做一轮补充验证并且测试时不光看慢镜头回放的行不行还要打开开发者选项里的模拟慢网络和模拟弱CPU验证改造后的页面在双弱环境下的表现。多数异步优化在强机上体现不明显恰恰是低端机上的流畅度提升才最能证明改动价值。这里说一个量化判断的小技巧做一个性能画像追踪异步加载的改动效果有三个维度可以梳理第一个维度是时间序列看加载过程的每个阶段耗时寻找瓶颈集中在哪段第二个维度是体积统计页面初始下载的资源总量和脚本总字节数第三个维度是内容可见速度统计用户第一眼能看到的实际内容出现时间。三个维度结合优化报告不仅有说服力改造方向也更清晰。7. 移动端异步加载的注意事项与踩坑实例7.1 弱网环境下的表现差异优化方案不能只在线下好用异步加载方案在强网下表现良好但在弱网下可能会出现非预期的问题。移动端的弱网条件不只是慢而是抖动严重——连接不稳定、带宽变化大、丢包率高等。在这种环境下按需加载的多请求策略可能变成坏事因为每个请求都得重新建立连接、过一遍拥塞控制多个小请求比一个合并的大请求慢得多。这里典型场景是路由懒加载。在弱网3G环境下页面切换时路由chunk请求的加载时间可能长达几秒用户点一个按钮后页面白屏半分钟。这在桌面开发环境里几乎感知不到因为你的开发机连接的是办公网、千兆内网但在用户现场就是实打实的痛点。处理这类问题有两个思路。第一是降低分包粒度让每个chunk尽可能小减少单个请求的体积。第二是提前预加载利用空闲时间把用户可能访问的路由资源提前拉取下来等用户真的跳转时就只剩执行成本了。这里补充一个小技巧在移动端项目里我会监测网络状态网络较差时自动降级为更少的分包方案用预估缓存把这些关键资源塞进更早的加载时机而不是等到用户操作才去取。7.2 异步执行顺序的陷阱依赖关系不能被打破异步加载天然打破了代码的来源顺序和加载顺序这给那些依赖全局顺序的项目埋下了一颗雷。如果A脚本修改了一个全局变量B脚本要用这个变量这在同步加载时代是不会出问题的换成defer和async后B脚本可能在A脚本之前加载完、执行完直接拿不到变量导致报错。我在一个老项目上就吃过这个亏。页面里有一段公共代码往window上挂了一个全局配置对象紧接着的组件代码要用这个配置。为了优化首屏我把组件代码改成了动态import加载结果组件经常报“Cannot read property of undefined”就是因为公共代码还在加载中组件已经拿到并开始执行了。解决方案要在架构层面解决不能靠脚本顺序碰运气。现在ES Module的静态导入和动态导入本身就有依赖解析能力静态导入的模块会保证先加载执行动态导入的模块虽然有延迟但加载完成后会保证模块内部依赖已就绪。真正要临记的是按照模块依赖关系组织代码而不是按标签顺序组织代码。老代码改造时如果没法立刻模块化至少要让基础公共库不变更顺序规则保持同步加载在这之上才敢对其他内容做异步化。表格化地比较常见的依赖陷阱和规避方式依赖类型风险点规避方案全局变量依赖异步脚本执行时全局对象未就绪初始化公共库用同步加载不参与异步化事件绑定顺序依赖异步脚本在事件绑定后执行监听不到早发事件用事件委托或状态标志检测是否已绑定样式与DOM结构依赖异步脚本执行时DOM节点还未插入确认执行时机在后置事件之后7.3 首屏白屏时间的权衡异步优化不能以体验为代价异步加载解决了资源阻塞问题但也带来了一个隐蔽的新代价额外的请求环节和更长的资源等待链路。尤其在移动端每多一个网络请求都要经历一次DNS解析、TCP握手如果是HTTPS还要加TLS握手。如果资源本身不大这些握手成本甚至超过下载成本。这种权衡在实践中就体现在“资源到底要不要拆分”的决策上。一个3KB的图标小程序单独拆包未必划算——网络握手比资源下载还耗时。正确做法是把小体积、高复用度的资源留在主包只把大体积、低频访问的资源做异步化。我把这个决策标准理解为一条法则异步化改造的收益取决于“省下的体积”和“额外引入的请求成本”之间的差距。省下的体积越大、请求频率越低异步化的价值越高。再有就是异步加载期间可能出现白屏时间。页面切换时如果先卸载当前视图再加载新路由如果新路由资源没拉下来页面就会全空。业界方案是提前预加载或者在新视图渲染前保留旧视图作为骨架屏。Vue和React生态都有对应的状态组件方案本质都是用一个占位骨架先把结构画出来把等待的时间从“无反馈的白屏”变成“有内容轮廓的等待”用户感知的时间大幅缩短。我自己在移动端H5项目中常用一个顺手的方案——页面初始化时先展示静态骨架框架数据加载完成后替换真实内容。这个方案的潜台词是直接把加载过程中的视觉预期提前告诉用户让异步加载带来的等待时间被感知地缩短。纯前端技术栈里没有银弹但让等待更有信息量已经是低成本高回报的基础操作了。8. 异步加载的前沿视角预加载、预连接与请求优先级控制异步加载不只是“晚点加载”更高级的操作是在合适的时间点“提前加载”。浏览器原生提供了一批预加载原语它们同样属于异步加载的范畴——资源加载不阻塞主线程可以在页面空闲时悄悄完成。比如摆在整个移动端性能优化页面里的资源若都属于异步加载体系就可以进一步做一个分层谁早加载、谁晚加载、谁空闲时加载。preload让浏览器提前请求当前页面应立即使用的关键资源。它的优先级较高通常会阻塞关键路径但如果用它加载的是字体文件或首屏图片收益就很明显——字形和关键图比别的资源更早到位。prefetch则用于提前拉取用户下一步极可能访问的资源比如用户停留在首页时预加载详情页的数据用户真的点击跳转时就能立刻渲染。更基础的preconnect和dns-prefetch用于提前建立网络连接缩短时间。!-- 提前连接可能用到的跨域源 -- link relpreconnect hrefhttps://api.example.com !-- 预加载当前页面需要的某个关键资源 -- link relpreload asimage href/images/hero.jpg !-- 预取用户下一步可能要访问的分包资源 -- link relprefetch href/chunks/detail-page.jspreload和prefetch的使用需要克制。过度预取各种资源会消耗移动端的流量和电量反而造成性能负优化。我的使用标准是只预加载首屏确定要用的资源、确定用户下一步会访问的资源不确定的一律不加。浏览器还会自动根据资源类型和位置分配请求优先级JavaScript和CSS请求优先级较高图片和异步加载的脚本优先级较低。Chrome 新版浏览器甚至允许通过fetch或XHR的priority参数直接控制请求优先级。不过这类内建优化机制新旧版本在各端支持不一实际项目中更可控的做法是在代码层自己做一次请求优先级管理。比如做一个轻量的请求调度队列把页面加载必需的最高优先级请求放进立即执行队列把预加载请求放进空闲队列把低优先级上报请求干脆延后到空闲时间。这种调度在移动端优化里非常实用。一个活动页会有七八个埋点请求这些请求既不紧急也不关键如果和首屏核心接口同时发出会抢占有限的网络带宽。把它们统一排队等页面可交互后再发出核心接口的响应时间就会有可感知的改善。异步加载的精细度决定优化的上限优先级管理就是精细度里最值得投入的一部分。我在实际项目中的应用习惯是做一个极简的请求管理器按网络任务的重要程度分三个优先级池critical首屏必需、normal用户交互后需要、idle可延后。遇到网络切换或者弱网检测时自动暂停idle池的流量把通道优先让给critical池。这套思路已经在几个移动端项目里实际落地对弱网下首屏速度提升明显。9. 性能优化的边界什么时候异步加载不再是答案异步加载好用但它不是解决所有性能问题的银弹。一个经常被忽略的边界条件是当资源总量本身就超出网络承载能力时异步加载只是在“掩盖”问题并没有解决它。比如一个页面下方放了十张高清大图每张三兆多懒加载确实让首屏快了但用户往下滑动时每张都要等好几秒体验依然很糟。这时候该做的是图像本身——压缩格式、渐进式编码、剪裁不同尺寸版本把资源体积真实降下来而不是指望加载时机调一调就万事大吉。再有就是当项目的瓶颈在服务端时异步加载无从下手。如果接口响应要两秒钟客户端怎么优化加载时机都是杯水车薪得从后端缓存、数据库查询、CDN节点这些方向去解决。性能优化要按全局视角去看前端加载只是其中一环。异步加载真正合理的边界在体感层面。我常和团队说一句经验之谈如果页面首屏的关键内容都已经能快速绘制就不要为了视觉上的“更炫”再把不关键的内容异步化。一切改动都应该以可衡量的指标为依据加了预加载、懒加载、异步脚本之后指标变好才值得保留指标没变化甚至变差就应该回滚。性能优化是工程不是仪式每一次改动都要有它的数据理由。最后聊一个我在移动端性能优化里反复验证过的经验优化的价值顺序永远先是“减少工作”再是“打散工作”最后才是“调整工作时机”。异步加载属于调整工作时机的手段它的效果建立在资源和任务总量已经合理的基础之上。先做减法再做调度顺序一定不能反。这个思路可能不会让你立刻看到惊艳的Demo效果但在生产环境的长期运行里它能帮你和你的页面少踩很多没必要的坑。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →