preload / prefetch / async / defer 核心区别与Next.js实战指南(面试专用)
适配场景:前端Leader岗面试、Web3金融交易场景性能优化、Next.js项目深度调优,覆盖99%的面试考点,从底层原理到实战落地全链路拆解。
一、核心本质分类(面试第一题必答,拉开差距)
很多面试者会把四个概念混为一谈,面试官考察的第一核心,就是你能否清晰区分两类完全不同的技术能力:
| 分类 | 包含属性 | 核心作用 | 解决的核心问题 |
|---|---|---|---|
| 脚本加载执行控制 | async / defer | 仅作用于<script>标签,控制JS脚本的下载时机、执行时机、是否阻塞页面渲染 | 解决原生JS脚本默认阻塞HTML解析和页面渲染的性能问题 |
| 资源预加载指令 | preload / prefetch | 作用于<link>标签,控制浏览器对各类资源的预加载行为、优先级、缓存策略,仅负责加载不负责执行 | 解决关键资源加载时机晚、页面渲染/交互卡顿、跳转页面白屏的体验问题 |
二、逐点深度拆解(底层原理+执行时机+适用场景)
(一)脚本加载控制:async vs defer
前置基础:原生脚本的默认行为
浏览器解析HTML的默认流程:
HTML解析 → 遇到普通<script>标签 → 暂停HTML解析 → 同步下载脚本 → 立即执行脚本 → 恢复HTML解析 → 解析完成触发DOMContentLoaded → 所有资源加载完成触发load
核心问题:普通脚本会完全阻塞HTML解析和渲染,脚本下载+执行的时间内,页面完全白屏,是首屏性能的核心瓶颈。
async和defer的核心价值,就是让脚本的下载过程完全不阻塞HTML解析,仅对执行时机做不同控制。
1. defer(延迟执行)
底层原理
- 下载阶段:遇到带
defer的脚本,浏览器异步后台下载脚本,完全不暂停HTML解析; - 执行阶段:脚本下载完成后,不会立即执行,会等待HTML完全解析完成、DOM树构建完毕,在
DOMContentLoaded事件触发之前,严格按照脚本在HTML中的出现顺序依次执行; - 多个
defer脚本:永远按标签出现顺序执行,不会因为下载快慢打乱顺序,依赖关系可正常生效。
执行时间线
HTML解析开始 → 遇到defer脚本 → 异步下载(不暂停解析)→ 继续解析HTML直到完全完成 → 按顺序执行所有defer脚本 → 触发DOMContentLoaded → 触发load事件
核心特性
- 永远不阻塞HTML解析,仅在HTML解析完成后执行;
- 严格保证执行顺序,适合有依赖关系的脚本;
- 执行时DOM已完全构建完成,可正常获取DOM节点;
- 兼容性:IE9+全支持,现代浏览器100%兼容。
适用场景
- 有严格依赖关系的核心业务脚本(如框架入口、全局状态、工具库);
- 需要操作DOM、且不阻塞首屏渲染的脚本;
- 必须在页面水合/交互前执行,但不阻塞首屏渲染的脚本。
2. async(异步执行)
底层原理
- 下载阶段:和
defer一致,遇到带async的脚本,浏览器异步后台下载,完全不暂停HTML解析; - 执行阶段:脚本下载完成后,立即暂停HTML解析,强制执行脚本,执行完成后再恢复HTML解析;
- 多个
async脚本:执行顺序完全不确定,哪个脚本先下载完成,就先执行哪个,和标签出现顺序无关。
执行时间线
HTML解析开始 → 遇到async脚本 → 异步下载(不暂停解析)→ 脚本下载完成 → 暂停HTML解析 → 执行脚本 → 恢复HTML解析 → 解析完成 → 触发DOMContentLoaded → 触发load事件
核心特性
- 下载不阻塞HTML解析,但执行会阻塞HTML解析;
- 执行顺序不可控,完全由下载速度决定,不适合有依赖关系的脚本;
- 执行时机不确定,可能在HTML解析完成前/后执行,无法保证DOM已完全构建;
- 兼容性:IE10+全支持,现代浏览器100%兼容。
适用场景
- 无任何依赖的独立第三方脚本(如统计、埋点、广告、客服工具);
- 不依赖DOM、不依赖其他脚本的独立功能脚本;
- 对执行时机无严格要求,不影响核心业务的非关键脚本。
async vs defer 核心区别对照表(面试必背)
| 对比维度 | defer | async |
|---|---|---|
| 下载是否阻塞HTML解析 | 不阻塞 | 不阻塞 |
| 执行是否阻塞HTML解析 | 不阻塞(HTML解析完成后才执行) | 阻塞(下载完成后立即暂停解析执行) |
| 执行顺序 | 严格按标签在HTML中的出现顺序执行 | 无顺序,先下载完成先执行 |
| 执行时机 | HTML完全解析完成后,DOMContentLoaded之前 | 脚本下载完成后立即执行,与HTML解析并行 |
| DOM可用性 | 执行时DOM已完全构建,可正常操作DOM | 执行时DOM可能未完成构建,无法保证DOM可用性 |
| 重复执行 | 多次添加相同脚本,仅执行一次 | 多次添加相同脚本,会多次下载多次执行 |
| 核心适用场景 | 有依赖关系的核心业务脚本、需要操作DOM的脚本 | 无依赖的第三方独立脚本、非关键功能脚本 |
(二)资源预加载指令:preload vs prefetch
两者都是通过<link rel="xxx">实现的浏览器预加载能力,核心区别是优先级、加载时机、使用场景完全不同。
1. preload(预加载当前页关键资源)
底层原理
preload是浏览器的高优先级强制预加载指令,告诉浏览器:这个资源是当前页面必须的关键资源,必须立即以最高优先级下载,缓存到内存中,等到需要使用时,直接从缓存读取,无需等待下载。
- 核心特点:只加载,不执行。预加载的JS/CSS/字体/图片,不会自动执行/渲染,必须等到页面正式引用时才会生效;
- 优先级:浏览器会给
preload资源最高的网络优先级,不会被其他低优先级资源抢占带宽; - 生命周期:预加载的资源如果在30秒内没有被页面使用,浏览器会自动丢弃,发出警告,造成带宽浪费。
基础语法
<!-- 预加载JS脚本 -->
<link rel="preload" href="/core-trade.js" as="script" />
<!-- 预加载字体文件(必须加crossorigin) -->
<link rel="preload" href="/font/trade-font.woff2" as="font" type="font/woff2" crossorigin />
<!-- 预加载图片 -->
<link rel="preload" href="/kline-bg.png" as="image" />
<!-- 预加载CSS -->
<link rel="preload" href="/core-trade.css" as="style" />
关键注意:
as属性必须填写,告诉浏览器资源类型,否则浏览器会给与很低的优先级,预加载失效。
核心特性
- 高优先级强制加载,不阻塞HTML解析和渲染;
- 提前加载关键资源,消除资源加载的延迟,优化LCP、FID等核心Web指标;
- 支持跨域资源,需添加
crossorigin属性; - 兼容性:IE不支持,现代浏览器(Chrome/Firefox/Safari 11.1+)全支持。
适用场景
- 当前页面首屏必须的关键资源(如核心业务JS、首屏字体、LCP大图、核心CSS);
- 加载时机晚的隐藏资源(如弹窗内的核心脚本、Tab切换后的内容);
- 金融交易场景的K线图、深度图核心渲染脚本,提前预加载保证交互秒开;
- 字体文件预加载,避免页面文字闪烁(FOIT/FOUT),优化CLS指标。
2. prefetch(预加载未来页面的资源)
底层原理
prefetch是浏览器的低优先级闲时预加载指令,告诉浏览器:这个资源是用户未来可能访问的下一个页面需要的,在浏览器当前页面空闲、带宽充足时,后台低优先级下载,缓存到磁盘中,用户跳转到对应页面时,直接从缓存读取。
- 核心特点:闲时加载,不影响当前页面。浏览器只会在主线程空闲、网络带宽富余时,才会下载prefetch资源,完全不会抢占当前页面关键资源的带宽;
- 优先级:网络优先级最低,不会影响当前页面的加载和渲染;
- 生命周期:预加载的资源会缓存到磁盘中,缓存时间由浏览器的HTTP缓存规则控制,短期(5分钟内)跳转页面可直接复用。
基础语法
<!-- 预加载未来页面的JS脚本 -->
<link rel="prefetch" href="/next-page-trade.js" as="script" />
<!-- 预加载未来页面的图片 -->
<link rel="prefetch" href="/next-page-kline.png" as="image" />
核心特性
- 低优先级闲时加载,完全不影响当前页面性能;
- 针对未来页面的资源预加载,大幅提升页面跳转的加载速度,实现秒开;
- 浏览器不保证一定会加载,若当前页面一直处于繁忙状态,会放弃prefetch;
- 兼容性:IE不支持,现代浏览器全支持。
适用场景
- 用户高概率会访问的下一个页面的核心资源(如首页→交易页、列表页→详情页);
- 新手引导、分步流程的下一步页面资源;
- 金融交易场景中,行情列表页预加载交易对详情页的核心脚本和数据;
- 高频访问的二级页面资源,提前预加载提升用户体验。
preload vs prefetch 核心区别对照表(面试必背)
| 对比维度 | preload | prefetch |
|---|---|---|
| 核心目的 | 提前加载当前页面必须的关键资源 | 提前加载未来页面可能用到的资源 |
| 加载优先级 | 最高优先级,浏览器强制立即加载 | 最低优先级,浏览器闲时才会加载 |
| 阻塞特性 | 不阻塞HTML解析和渲染,不阻塞window.onload | 完全不影响当前页面的任何加载和渲染 |
| 缓存策略 | 加载后缓存到内存,30秒内未使用会被丢弃 | 加载后缓存到磁盘,按HTTP缓存规则长期存储 |
| 适用页面 | 仅针对当前页面 | 针对未来的下一个页面 |
| 滥用风险 | 高,过度使用会抢占关键资源带宽,导致首屏性能下降 | 低,仅在闲时加载,过度使用仅会浪费用户带宽 |
| 核心优化目标 | 优化当前页面的首屏渲染速度、LCP、交互响应速度 | 优化未来页面的跳转加载速度,实现页面秒开 |
三、Next.js中的专属使用方式与最佳实践
Next.js对上述四个能力做了深度封装和优化,提供了专属API,无需手动写原生标签,同时内置了大量预加载优化,面试中重点考察你对Next.js封装API的理解和最佳实践。
(一)async / defer 在Next.js中的使用:next/script 组件
Next.js官方强烈推荐使用next/script组件替代原生<script>标签,该组件内置了对async/defer的深度封装,通过strategy属性精准控制脚本的加载执行时机,完美适配SSR/SSG/Streaming渲染模式。
1. strategy 属性与 async/defer 的对应关系
| strategy 取值 | 对应原生能力 | 核心行为 | 适用场景 | 使用限制 |
|---|---|---|---|---|
beforeInteractive | 强化版 defer | 在HTML流式传输之前加载脚本,在页面水合之前同步执行,阻塞水合但不阻塞HTML渲染 | 核心polyfill、全局配置、必须在水合前执行的脚本 | 仅能在Pages Router的_document.js 或 App Router的根layout.js中使用 |
afterInteractive(默认值) | 标准 defer | 页面水合完成后异步加载脚本,不阻塞HTML解析和渲染,在DOMContentLoaded之后执行 | 第三方统计、埋点、分析工具、非核心业务脚本 | 可在任何页面/组件中使用 |
lazyOnload | 闲时版 async | 浏览器完全空闲时(window.load之后)才加载脚本,完全不影响当前页面的任何性能 | 非关键的第三方脚本(客服、聊天工具、广告、低频功能) | 可在任何页面/组件中使用 |
worker | 专属Web Worker | 将脚本加载到Web Worker中执行,完全不阻塞主线程 | heavy计算脚本(行情数据计算、加密解密) | Next.js 14+ 支持,需配合App Router |
2. 原生 async/defer 的直接使用
next/script组件也支持直接添加async/defer属性,和原生行为完全一致:
import Script from 'next/script';
// 等同于原生带async的script
<Script src="https://third-party-analytics.js" async />
// 等同于原生带defer的script
<Script src="https://core-utils.js" defer />
3. 金融交易场景最佳实践
// App Router 根 layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
<html lang="zh-CN">
<body>
{children}
{/* 1. 必须在水合前执行的行情加密工具库,用beforeInteractive */}
<Script
src="/libs/trade-encrypt.js"
strategy="beforeInteractive"
/>
{/* 2. 统计埋点脚本,水合后加载,不阻塞渲染 */}
<Script
src="https://analytics.example.com/script.js"
strategy="afterInteractive"
/>
{/* 3. 在线客服脚本,闲时加载,不影响核心交易功能 */}
<Script
src="https://kefu.example.com/widget.js"
strategy="lazyOnload"
/>
</body>
</html>
);
}
(二)preload / prefetch 在Next.js中的使用
Next.js内置了大量自动预加载优化,同时提供了手动控制的API,是面试的高频考点。
1. 页面级prefetch:next/link 组件
这是Next.js最核心、最常用的预加载能力,也是面试必考点。
核心行为
- 默认行为:
<Link>组件包裹的路由链接,只要出现在浏览器视口内,Next.js会自动prefetch目标页面的JS代码和静态数据; - App Router 特性:prefetch仅预加载页面的静态部分,动态服务端组件不会预加载,跳转时才会请求动态数据;
- Pages Router 特性:prefetch会预加载整个页面的JS代码和
getStaticProps的静态数据; - 生效范围:仅在生产环境生效,开发环境不会自动prefetch;
- 手动控制:通过
prefetch={false}关闭自动预加载,减少带宽浪费。
实战用法
import Link from 'next/link';
export default function HomePage() {
return (
<div>
{/* 1. 默认开启prefetch:交易页是用户高概率点击的,自动预加载,跳转秒开 */}
<Link href="/trade/btc-usdt">
BTC/USDT 交易页
</Link>
{/* 2. 关闭prefetch:低频访问的设置页、帮助中心,避免浪费带宽 */}
<Link href="/settings" prefetch={false}>
系统设置
</Link>
{/* 3. 强制prefetch:隐藏的高频页面,手动强制预加载 */}
<Link href="/kline-fullscreen" prefetch={true}>
全屏K线
</Link>
</div>
);
}
2. 资源级preload:Next.js 内置组件优化
Next.js对核心资源的preload做了内置封装,无需手动写<link>标签:
(1)字体预加载:next/font
Next.js官方字体组件默认开启preload,自动预加载页面使用的字体文件,避免文字闪烁,优化CLS指标:
import { Inter } from 'next/font/google';
// 自动preload字体,优化首屏渲染
const inter = Inter({
subsets: ['latin'],
preload: true, // 默认开启,可手动关闭
});
export default function RootLayout({ children }) {
return <html className={inter.className}>{children}</html>;
}
(2)图片预加载:next/image priority 属性
给next/image组件添加priority={true},Next.js会自动给图片设置preload,提升LCP指标,是首屏大图优化的官方推荐方案:
import Image from 'next/image';
export default function TradePage() {
return (
<div>
{/* 首屏LCP大图,开启priority,自动preload,优化LCP */}
<Image
src="/kline-main-bg.png"
width={1200}
height={600}
priority={true}
alt="K线图背景"
/>
</div>
);
}
3. 手动自定义preload/prefetch
对于特殊场景,Next.js支持手动添加原生preload/prefetch标签,同时提供了React 18+的预加载API:
(1)手动添加preload标签
// App Router 根 layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="zh-CN">
<head>
{/* 手动preload交易核心脚本 */}
<link rel="preload" href="/libs/kline-render.js" as="script" />
{/* 手动prefetch未来页面的资源 */}
<link rel="prefetch" href="/libs/setting-page.js" as="script" />
</head>
<body>{children}</body>
</html>
);
}
(2)React 18+ 预加载API
Next.js 13+ 支持React 18的preloadAPI,可在事件处理函数中提前预加载资源,比如鼠标hover到按钮上时,预加载目标页面的资源:
import { preload } from 'react-dom';
export default function TradeCard() {
const handleMouseEnter = () => {
// 鼠标hover时,提前预加载交易页的核心资源
preload('/trade/btc-usdt');
preload('/libs/kline-render.js', { as: 'script' });
};
return (
<Link
href="/trade/btc-usdt"
onMouseEnter={handleMouseEnter}
>
BTC/USDT 交易
</Link>
);
}
四、面试高频考点与标准答案
1. 请说一下async和defer的核心区别?
标准答案:
async和defer的核心共同点是脚本下载都不会阻塞HTML解析,核心区别有3点:
- 执行时机不同:async脚本下载完成后立即执行,会阻塞HTML解析;defer脚本会等待HTML完全解析完成后,在DOMContentLoaded之前执行,不会阻塞HTML解析。
- 执行顺序不同:多个defer脚本严格按照标签在HTML中的出现顺序执行,可保证依赖关系;多个async脚本的执行顺序完全不确定,谁先下载完成谁先执行,不适合有依赖关系的脚本。
- DOM可用性不同:defer脚本执行时,DOM已完全构建完成,可正常操作DOM;async脚本执行时机不确定,无法保证DOM已完成构建。
2. preload和prefetch的核心区别是什么?
标准答案:
两者的核心区别在于加载目的、优先级、适用场景完全不同:
- 核心目的不同:preload是为了提前加载当前页面必须的关键资源,优化当前页面的首屏性能;prefetch是为了提前加载未来页面可能用到的资源,优化后续页面的跳转速度。
- 加载优先级不同:preload是浏览器最高优先级的加载指令,会强制立即加载;prefetch是最低优先级,浏览器只会在闲时、带宽富余时才会加载。
- 缓存策略不同:preload的资源缓存到内存,30秒内未使用会被浏览器丢弃;prefetch的资源缓存到磁盘,按HTTP缓存规则长期存储。
- 滥用风险不同:preload滥用会抢占关键资源的带宽,导致首屏性能下降,风险极高;prefetch滥用仅会浪费用户带宽,对当前页面性能无影响。
3. Next.js中next/link的prefetch默认行为是什么?
标准答案:
next/link的prefetch默认行为分两种路由模式:
- App Router:默认
prefetch={true},当Link组件出现在浏览器视口内时,Next.js会自动预加载目标路由的静态部分,动态服务端组件不会预加载,仅在用户跳转时请求动态数据; - Pages Router:默认
prefetch={true},当Link组件出现在视口内时,Next.js会自动预加载目标页面的完整JS代码和getStaticProps的静态数据; - 补充说明:prefetch仅在生产环境生效,开发环境不会执行;对于动态路由、
prefetch={false}的Link,不会自动预加载。
4. 带async/defer的脚本,会阻塞DOMContentLoaded和load事件吗?
标准答案:
- defer脚本:会在DOMContentLoaded事件触发之前执行,会延迟DOMContentLoaded事件的触发,直到所有defer脚本执行完成;不会阻塞load事件。
- async脚本:执行时机不确定,若在HTML解析完成前执行完成,会阻塞DOMContentLoaded;若在HTML解析完成后执行,不会阻塞DOMContentLoaded;无论何时执行,都会阻塞load事件,直到所有async脚本执行完成。
5. 滥用preload会有什么问题?
标准答案:
滥用preload会带来3个核心问题:
- 带宽抢占:preload会占用最高的网络优先级,过度使用会抢占当前页面关键资源(如首屏JS、CSS、LCP图片)的带宽,反而导致首屏加载变慢,核心指标恶化。
- 内存浪费:preload的资源如果30秒内没有被页面使用,会被浏览器自动丢弃,造成用户带宽和内存的浪费,尤其对移动端流量不友好。
- 性能负优化:大量preload请求会导致TCP连接拥塞,关键资源的加载延迟增加,同时浏览器的预加载扫描器会失效,反而造成页面渲染卡顿。
五、踩坑避坑指南(面试加分项)
- async脚本依赖问题:不要给有依赖关系的脚本加async,比如a.js依赖b.js,两个都加async,若a.js先下载完成,会先执行,此时b.js还未加载,会报undefined错误,这类脚本必须用defer。
- preload资源必须使用:preload的资源必须在当前页面被引用,否则浏览器会发出警告,30秒后丢弃资源,造成带宽浪费。
- preload字体必须加crossorigin:预加载字体文件时,必须添加
crossorigin属性,哪怕是同域字体,否则浏览器会重复下载,预加载失效。 - defer脚本不要监听DOMContentLoaded:defer脚本会在DOMContentLoaded之前执行,脚本内监听DOMContentLoaded事件,永远不会触发。
- Next.js中不要过度prefetch:不要给所有Link都开启prefetch,低频访问的页面必须关闭prefetch,避免浪费用户带宽,尤其对移动端用户不友好。
- 不要用preload替代prefetch:preload是给当前页面关键资源用的,未来页面的资源必须用prefetch,否则会抢占当前页面的带宽,造成性能负优化。
六、金融交易场景专属最佳实践
- 交易页核心资源preload:对K线图渲染脚本、交易核心SDK、首屏LCP图片,用preload/priority提前加载,保证交易页打开秒渲染,交互无延迟。
- 行情列表页prefetch交易页:行情卡片用next/link包裹,开启prefetch,用户hover/看到行情卡片时,自动预加载对应交易对的交易页资源,点击跳转秒开。
- 脚本分级加载:核心交易加密、行情计算脚本用
beforeInteractive/defer,保证执行顺序和依赖关系;统计、客服等非关键脚本用lazyOnload,不阻塞核心交易功能。 - 动态内容预加载:用户鼠标hover到行情卡片时,通过React preload API提前预加载K线历史数据,用户进入交易页时,数据已缓存,K线图秒渲染。
- 低频页面关闭prefetch:设置页、帮助中心、关于我们等低频访问页面,给next/link设置
prefetch={false},减少带宽占用,避免影响核心交易页面的性能。