懒加载与预加载
懒加载(Lazy Load)
懒加载的核心思想:延迟非首屏资源的加载时机,优先保障首屏渲染速度。
在图片量较大的页面(电商首页、社交信息流、图库等),一次性加载所有资源会导致:
- 白屏时间过长 — 首屏内容被大量后续图片请求阻塞
- 带宽浪费 — 用户可能根本不会滚动到页面底部
- 内存压力 — 大量 DOM 节点和图片同时存在于内存中
懒加载策略:只加载当前可视区域(及附近)的资源,用户滚动到对应位置时再即时加载。
图片懒加载原理
核心思路:
- 将真实
src存储在data-src属性中(不触发请求) img标签使用占位符或空白状态- 监听
scroll事件,检测元素是否进入可视区域 - 进入可视区域时,将
data-src赋值给src,触发真实加载
关键 API:
// 可视区域高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight
// 元素相对于视口的位置
const rect = element.getBoundingClientRect()
// rect.top: 元素顶部距离视口顶部的距离
// rect.bottom: 元素底部距离视口顶部的距离
// rect.left / rect.right: 水平方向位置
// 判断元素是否在可视区域内
const isVisible = rect.top < viewHeight && rect.bottom > 0IntersectionObserver API
IntersectionObserver API 提供了异步观察元素与祖先元素(或视口)交叉状态的能力,比 scroll 事件更高效。
const imgs = document.querySelectorAll("img[data-src]")
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.getAttribute("data-src")
img.removeAttribute("data-src")
obs.unobserve(img) // 停止观察已加载的元素
}
})
}, {
rootMargin: "0px 0px 200px 0px", // 提前 200px 开始加载
threshold: 0.01 // 只要 1% 可见就触发
})
imgs.forEach((img) => observer.observe(img))Scroll 事件 vs IntersectionObserver 对比:
| 维度 | Scroll 事件 | IntersectionObserver |
|---|---|---|
| 性能 | 每次 scroll 都触发回调 | 浏览器内部优化,按需通知 |
| 实现复杂度 | 需手动计算位置 | 声明式 API |
| 兼容性 | 全兼容 | IE 不支持(需 polyfill) |
| 推荐度 | 基础场景可用 | 推荐使用 |
原生 loading="lazy" 属性
现代浏览器支持原生的图片懒加载属性:
<!-- 浏览器自动处理懒加载逻辑 -->
<img src="image.jpg" alt="描述" loading="lazy" />
<!-- 可选值:
lazy — 进入视口附近时才加载
eager — 立即加载(默认行为)
auto — 由浏览器决定
-->浏览器支持情况:Chrome 77+、Firefox 75+、Edge 79+、Safari 15.4+
代码示例
基础实现(scroll 事件监听):
<!-- HTML 结构省略,仅展示关键 JS 逻辑 -->扩展应用场景:
| 场景 | 方案 |
|---|---|
| 图片懒加载 | loading="lazy" 或 IntersectionObserver |
| 路由懒加载 | React.lazy + Suspense / Vue 异步组件 |
| 组件懒加载 | 动态 import() + 代码分割 |
| 数据列表虚拟滚动 | 只渲染可见行(react-virtualized 等) |
性能注意事项:
scroll 事件是高频事件,需要注意:
- 使用
{ passive: true }标记避免阻塞滚动 - 使用
requestAnimationFrame节流回调执行频率 - 优先考虑 IntersectionObserver 替代 scroll 监听
- 图片加载完成后及时移除事件监听和 Observer
预加载(Preload / Prefetch)
预加载与懒加载相反,它的核心思想是提前加载未来需要的资源,减少用户等待时间。
<link rel="preload">
preload 用于提前加载当前页面必定需要的资源,优先级高,浏览器会立即开始下载。
<!-- 预加载关键 CSS -->
<link rel="preload" href="/styles/critical.css" as="style" />
<!-- 预加载关键 JavaScript -->
<link rel="preload" href="/scripts/app.js" as="script" />
<!-- 预加载字体文件(跨域资源需加 crossorigin) -->
<link rel="preload" href="/fonts/roboto.woff2" as="font" type="font/woff2" crossorigin />
<!-- 预加载图片 -->
<link rel="preload" href="/images/hero.jpg" as="image" />as 属性的常用值:
as 值 | 资源类型 |
|---|---|
style | CSS 样式表 |
script | JavaScript |
font | 字体文件 |
image | 图片 |
fetch | fetch/XHR 请求 |
document | HTML 文档 |
注意事项:
- 必须指定
as属性,否则浏览器无法正确设置优先级 - 预加载字体时必须添加
crossorigin属性,否则会重复下载 - 预加载的资源如果 3 秒内未被使用,控制台会发出警告
- 不要滥用
preload,只预加载关键资源
<link rel="prefetch">
prefetch 用于提前加载未来可能需要的资源(如下一页的资源),优先级低,浏览器在空闲时才会下载。
<!-- 预获取下一页的关键资源 -->
<link rel="prefetch" href="/next-page/styles.css" />
<link rel="prefetch" href="/next-page/app.js" />
<!-- 预获取下一页的 HTML -->
<link rel="prefetch" href="/next-page.html" />preload vs prefetch 对比:
| 维度 | preload | prefetch |
|---|---|---|
| 优先级 | 高(立即下载) | 低(空闲时下载) |
| 适用场景 | 当前页面必需资源 | 未来页面可能需要的资源 |
| 资源使用 | 必须在当前页面使用 | 供后续导航使用 |
| 缓存行为 | 缓存后立即可用 | 缓存后供后续请求使用 |
<link rel="preconnect">
preconnect 用于提前建立与目标服务器的连接(DNS 查询、TCP 握手、TLS 协商),减少后续请求的延迟。
<!-- 预连接到 CDN -->
<link rel="preconnect" href="https://cdn.example.com" />
<!-- 预连接到 API 服务器 -->
<link rel="preconnect" href="https://api.example.com" />
<!-- 仅预解析 DNS(更轻量,适合低优先级域名) -->
<link rel="dns-prefetch" href="https://cdn.example.com" />preconnect vs dns-prefetch 对比:
| 维度 | preconnect | dns-prefetch |
|---|---|---|
| 操作 | DNS + TCP + TLS | 仅 DNS 解析 |
| 开销 | 较高(占用连接资源) | 极低 |
| 适用场景 | 高优先级第三方域名 | 低优先级或备用域名 |
使用场景与注意事项
典型使用场景:
| 场景 | 推荐策略 |
|---|---|
| 首屏关键 CSS/JS | preload |
| Web 字体文件 | preload + crossorigin |
| 下一页资源 | prefetch |
| 第三方 CDN 域名 | preconnect |
| 低优先级第三方域名 | dns-prefetch |
注意事项:
preload不要滥用,只预加载真正关键的首屏资源,否则会挤占带宽影响首屏渲染prefetch的资源在空闲时下载,如果用户快速离开页面,预取的资源可能浪费带宽preconnect建议限制在 3-4 个域名以内,过多会浪费连接资源- 预加载的资源如果长时间未被使用,浏览器会发出警告并可能丢弃缓存
- 在 HTTP/2 环境下,浏览器本身已经具备一定的资源优先级调度能力,需评估预加载的实际收益
懒加载 vs 预加载对比
| 维度 | 懒加载 | 预加载 |
|---|---|---|
| 核心思想 | 延迟加载,按需请求 | 提前加载,预判需求 |
| 目标 | 减少首屏资源体积,加快首屏渲染 | 减少后续交互等待时间 |
| 适用资源 | 非首屏图片、路由组件、长列表 | 关键 CSS/JS、字体、下一页资源 |
| 优先级 | 低(用户滚动到才加载) | 高(立即或空闲时下载) |
| 风险 | 滚动时可能出现短暂空白 | 可能浪费带宽(预加载未使用资源) |
| 典型技术 | IntersectionObserver、loading="lazy" | <link rel="preload/prefetch"> |
最佳实践:懒加载与预加载配合使用——对首屏非关键资源使用懒加载减少首屏负担,对首屏关键资源和即将需要的资源使用预加载缩短等待时间。
预请求、预加载与预渲染实战
在接口耗时不可忽略的场景(如机票、酒店等需要实时数据的业务),为了做到极致秒开,可以综合运用预请求、预加载、预渲染三种黑科技。
1. 预请求(Pre-request)
预请求是指对后端请求参数的统一拼装。核心是统一参数拼装逻辑,避免为预请求单独做一套流程(既易出错又拉长首屏时间)。
以机票业务为例:进入列表页后,前端解析 URL 路径拿到参数(from=shanghai&to=beijing&date=20210606),调用 Native schema 进入参数解析,拼装成完整请求参数。通过封装 preReq 功能,用同一份代码完成参数拼装。
- 若已使用 Native 统一请求,直接走客户端逻辑发送。
- 若未走 Native 统一请求,可借助 Axios 库函数:在
post/get前添加BeforeFetch钩子,对 URL 参数解析和 Native 参数补全。
// Axios 封装:BeforeFetch 钩子做参数拼装
function createFetcher() {
// 在请求前统一解析 URL 参数、补全 Native 参数
axios.interceptors.request.use((config) => {
config.params = { ...config.params, ...resolveNativeParams(config.url) };
return config;
});
return axios;
}2. 预加载(Pre-fetch)
预加载是对后端数据接口的提前加载。关键在于把握预加载时机——通过判断用户操作是否命中特定路径。
以机票列表页为例,操作路径有约定编号:进入首页=0、输入出发地目的地=1、输入日期=2、切换关键词=3、点击我的位置=4。后端在用户进入列表页时返回一个操作路径数组(如 [1,2,3]),当用户操作命中该数组时,就触发预加载,提前请求列表页接口。
实现方式:
- 若 Native 提供预加载接口,直接使用。
- 否则扩展 Axios,封装
afterFetch钩子,将加载完成的数据存储到本地供下一个路由使用。
// afterFetch 钩子:数据加载完成后缓存,供下一路由使用
async function fetchWithCache(url) {
// 先检查内存中是否有预加载数据
if (cache.has(url)) return cache.get(url);
const data = await axios.fetch(url);
cache.set(url, data); // 预加载并缓存
return data;
}即使预加载也要做好缓存处理:先在内存检查是否存在预加载数据,有则直接用,没有则走预加载逻辑并设置缓存。
3. 预渲染(Pre-render)
预渲染是在用户访问页面之前完成页面渲染准备。用户命中特定路径时,前端预先渲染好搜索结果页(放在可视区域下方不可见),用户点击搜索时直接将其推到顶层展示,省去初始化、请求数据和渲染的时间。
预渲染常用 NSR(Native Side Rendering,客户端渲染) 技术:通过客户端(Native 侧)进行页面结构拼接实现渲染。NSR 需要三要素:
- 模板准备:用离线包提供模板(HTML/JS/CSS),预请求和预加载提供数据。
- 页面与模板映射:页面动态而 URL 静态,需实现页面到模板的多对一映射机制。
- Native 本地渲染服务:在 Native 侧实现类似 SSR 的本地渲染服务。
端外场景:无法用离线包时,可使用 SSR 替代。另一个预渲染思路是虚拟页 T+2 预渲染——如文档平台展示第 T 页时,预先在可视范围外渲染 T+2 页,点击时直接移动回来。预渲染可能遇到内存问题,可精简预渲染内容(如图片延后获取)。
降级策略
预加载和预渲染需要降级兜底:没有拿到数据时回退 CSR;端外无法使用离线包时用 SSR 替代。