适配场景:前端Leader岗面试、Web3金融交易场景性能优化、Next.js项目深度调优,覆盖99%的面试考点,从底层原理到实战落地全链路拆解。
很多面试者会把四个概念混为一谈,面试官考察的第一核心,就是你能否清晰区分两类完全不同的技术能力:
| 分类 | 包含属性 | 核心作用 | 解决的核心问题 |
|---|---|---|---|
| 脚本加载执行控制 | async / defer | 仅作用于<script>标签,控制JS脚本的下载时机、执行时机、是否阻塞页面渲染 | 解决原生JS脚本默认阻塞HTML解析和页面渲染的性能问题 |
| 资源预加载指令 | preload / prefetch | 作用于<link>标签,控制浏览器对各类资源的预加载行为、优先级、缓存策略,仅负责加载不负责执行 | 解决关键资源加载时机晚、页面渲染/交互卡顿、跳转页面白屏的体验问题 |
浏览器解析HTML的默认流程:
HTML解析 → 遇到普通<script>标签 → 暂停HTML解析 → 同步下载脚本 → 立即执行脚本 → 恢复HTML解析 → 解析完成触发DOMContentLoaded → 所有资源加载完成触发load
核心问题:普通脚本会完全阻塞HTML解析和渲染,脚本下载+执行的时间内,页面完全白屏,是首屏性能的核心瓶颈。
async和defer的核心价值,就是让脚本的下载过程完全不阻塞HTML解析,仅对执行时机做不同控制。
defer的脚本,浏览器异步后台下载脚本,完全不暂停HTML解析;DOMContentLoaded事件触发之前,严格按照脚本在HTML中的出现顺序依次执行;defer脚本:永远按标签出现顺序执行,不会因为下载快慢打乱顺序,依赖关系可正常生效。HTML解析开始 → 遇到defer脚本 → 异步下载(不暂停解析)→ 继续解析HTML直到完全完成 → 按顺序执行所有defer脚本 → 触发DOMContentLoaded → 触发load事件
defer一致,遇到带async的脚本,浏览器异步后台下载,完全不暂停HTML解析;async脚本:执行顺序完全不确定,哪个脚本先下载完成,就先执行哪个,和标签出现顺序无关。HTML解析开始 → 遇到async脚本 → 异步下载(不暂停解析)→ 脚本下载完成 → 暂停HTML解析 → 执行脚本 → 恢复HTML解析 → 解析完成 → 触发DOMContentLoaded → 触发load事件
| 对比维度 | defer | async |
|---|---|---|
| 下载是否阻塞HTML解析 | 不阻塞 | 不阻塞 |
| 执行是否阻塞HTML解析 | 不阻塞(HTML解析完成后才执行) | 阻塞(下载完成后立即暂停解析执行) |
| 执行顺序 | 严格按标签在HTML中的出现顺序执行 | 无顺序,先下载完成先执行 |
| 执行时机 | HTML完全解析完成后,DOMContentLoaded之前 | 脚本下载完成后立即执行,与HTML解析并行 |
| DOM可用性 | 执行时DOM已完全构建,可正常操作DOM | 执行时DOM可能未完成构建,无法保证DOM可用性 |
| 重复执行 | 多次添加相同脚本,仅执行一次 | 多次添加相同脚本,会多次下载多次执行 |
| 核心适用场景 | 有依赖关系的核心业务脚本、需要操作DOM的脚本 | 无依赖的第三方独立脚本、非关键功能脚本 |
两者都是通过<link rel="xxx">实现的浏览器预加载能力,核心区别是优先级、加载时机、使用场景完全不同。
preload是浏览器的高优先级强制预加载指令,告诉浏览器:这个资源是当前页面必须的关键资源,必须立即以最高优先级下载,缓存到内存中,等到需要使用时,直接从缓存读取,无需等待下载。
preload资源最高的网络优先级,不会被其他低优先级资源抢占带宽;<!-- 预加载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属性必须填写,告诉浏览器资源类型,否则浏览器会给与很低的优先级,预加载失效。
crossorigin属性;prefetch是浏览器的低优先级闲时预加载指令,告诉浏览器:这个资源是用户未来可能访问的下一个页面需要的,在浏览器当前页面空闲、带宽充足时,后台低优先级下载,缓存到磁盘中,用户跳转到对应页面时,直接从缓存读取。
<!-- 预加载未来页面的JS脚本 -->
<link rel="prefetch" href="/next-page-trade.js" as="script" />
<!-- 预加载未来页面的图片 -->
<link rel="prefetch" href="/next-page-kline.png" as="image" />
| 对比维度 | preload | prefetch |
|---|---|---|
| 核心目的 | 提前加载当前页面必须的关键资源 | 提前加载未来页面可能用到的资源 |
| 加载优先级 | 最高优先级,浏览器强制立即加载 | 最低优先级,浏览器闲时才会加载 |
| 阻塞特性 | 不阻塞HTML解析和渲染,不阻塞window.onload | 完全不影响当前页面的任何加载和渲染 |
| 缓存策略 | 加载后缓存到内存,30秒内未使用会被丢弃 | 加载后缓存到磁盘,按HTTP缓存规则长期存储 |
| 适用页面 | 仅针对当前页面 | 针对未来的下一个页面 |
| 滥用风险 | 高,过度使用会抢占关键资源带宽,导致首屏性能下降 | 低,仅在闲时加载,过度使用仅会浪费用户带宽 |
| 核心优化目标 | 优化当前页面的首屏渲染速度、LCP、交互响应速度 | 优化未来页面的跳转加载速度,实现页面秒开 |
Next.js对上述四个能力做了深度封装和优化,提供了专属API,无需手动写原生标签,同时内置了大量预加载优化,面试中重点考察你对Next.js封装API的理解和最佳实践。
Next.js官方强烈推荐使用next/script组件替代原生<script>标签,该组件内置了对async/defer的深度封装,通过strategy属性精准控制脚本的加载执行时机,完美适配SSR/SSG/Streaming渲染模式。
| 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 |
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 />
// 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>
);
}
Next.js内置了大量自动预加载优化,同时提供了手动控制的API,是面试的高频考点。
这是Next.js最核心、最常用的预加载能力,也是面试必考点。
<Link>组件包裹的路由链接,只要出现在浏览器视口内,Next.js会自动prefetch目标页面的JS代码和静态数据;getStaticProps的静态数据;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>
);
}
Next.js对核心资源的preload做了内置封装,无需手动写<link>标签:
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>;
}
给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>
);
}
对于特殊场景,Next.js支持手动添加原生preload/prefetch标签,同时提供了React 18+的预加载API:
// 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>
);
}
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>
);
}
标准答案:
async和defer的核心共同点是脚本下载都不会阻塞HTML解析,核心区别有3点:
标准答案:
两者的核心区别在于加载目的、优先级、适用场景完全不同:
标准答案:
next/link的prefetch默认行为分两种路由模式:
prefetch={true},当Link组件出现在浏览器视口内时,Next.js会自动预加载目标路由的静态部分,动态服务端组件不会预加载,仅在用户跳转时请求动态数据;prefetch={true},当Link组件出现在视口内时,Next.js会自动预加载目标页面的完整JS代码和getStaticProps的静态数据;prefetch={false}的Link,不会自动预加载。标准答案:
标准答案:
滥用preload会带来3个核心问题:
crossorigin属性,哪怕是同域字体,否则浏览器会重复下载,预加载失效。beforeInteractive/defer,保证执行顺序和依赖关系;统计、客服等非关键脚本用lazyOnload,不阻塞核心交易功能。prefetch={false},减少带宽占用,避免影响核心交易页面的性能。