{T}

懒加载与预加载

懒加载(Lazy Load)

懒加载的核心思想:延迟非首屏资源的加载时机,优先保障首屏渲染速度。

在图片量较大的页面(电商首页、社交信息流、图库等),一次性加载所有资源会导致:

  • 白屏时间过长 — 首屏内容被大量后续图片请求阻塞
  • 带宽浪费 — 用户可能根本不会滚动到页面底部
  • 内存压力 — 大量 DOM 节点和图片同时存在于内存中

懒加载策略:只加载当前可视区域(及附近)的资源,用户滚动到对应位置时再即时加载。

图片懒加载原理

核心思路:

  1. 将真实 src 存储在 data-src 属性中(不触发请求)
  2. img 标签使用占位符或空白状态
  3. 监听 scroll 事件,检测元素是否进入可视区域
  4. 进入可视区域时,将 data-src 赋值给 src,触发真实加载

关键 API:

javascript
// 可视区域高度
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 > 0

IntersectionObserver API

IntersectionObserver API 提供了异步观察元素与祖先元素(或视口)交叉状态的能力,比 scroll 事件更高效。

javascript
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" 属性

现代浏览器支持原生的图片懒加载属性:

html
<!-- 浏览器自动处理懒加载逻辑 -->
<img src="image.jpg" alt="描述" loading="lazy" />

<!-- 可选值:
  lazy   — 进入视口附近时才加载
  eager  — 立即加载(默认行为)
  auto   — 由浏览器决定
-->

浏览器支持情况:Chrome 77+、Firefox 75+、Edge 79+、Safari 15.4+

代码示例

基础实现(scroll 事件监听):

html
<!-- HTML 结构省略,仅展示关键 JS 逻辑 -->

扩展应用场景

场景方案
图片懒加载loading="lazy" 或 IntersectionObserver
路由懒加载React.lazy + Suspense / Vue 异步组件
组件懒加载动态 import() + 代码分割
数据列表虚拟滚动只渲染可见行(react-virtualized 等)

性能注意事项

scroll 事件是高频事件,需要注意:

  • 使用 { passive: true } 标记避免阻塞滚动
  • 使用 requestAnimationFrame 节流回调执行频率
  • 优先考虑 IntersectionObserver 替代 scroll 监听
  • 图片加载完成后及时移除事件监听和 Observer

预加载(Preload / Prefetch)

预加载与懒加载相反,它的核心思想是提前加载未来需要的资源,减少用户等待时间。

preload 用于提前加载当前页面必定需要的资源,优先级高,浏览器会立即开始下载。

html
<!-- 预加载关键 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资源类型
styleCSS 样式表
scriptJavaScript
font字体文件
image图片
fetchfetch/XHR 请求
documentHTML 文档

注意事项

  • 必须指定 as 属性,否则浏览器无法正确设置优先级
  • 预加载字体时必须添加 crossorigin 属性,否则会重复下载
  • 预加载的资源如果 3 秒内未被使用,控制台会发出警告
  • 不要滥用 preload,只预加载关键资源

prefetch 用于提前加载未来可能需要的资源(如下一页的资源),优先级低,浏览器在空闲时才会下载。

html
<!-- 预获取下一页的关键资源 -->
<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 对比

维度preloadprefetch
优先级高(立即下载)低(空闲时下载)
适用场景当前页面必需资源未来页面可能需要的资源
资源使用必须在当前页面使用供后续导航使用
缓存行为缓存后立即可用缓存后供后续请求使用

preconnect 用于提前建立与目标服务器的连接(DNS 查询、TCP 握手、TLS 协商),减少后续请求的延迟。

html
<!-- 预连接到 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 对比

维度preconnectdns-prefetch
操作DNS + TCP + TLS仅 DNS 解析
开销较高(占用连接资源)极低
适用场景高优先级第三方域名低优先级或备用域名

使用场景与注意事项

典型使用场景

场景推荐策略
首屏关键 CSS/JSpreload
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 参数补全。
javascript
// 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 钩子,将加载完成的数据存储到本地供下一个路由使用。
javascript
// 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 需要三要素:

  1. 模板准备:用离线包提供模板(HTML/JS/CSS),预请求和预加载提供数据。
  2. 页面与模板映射:页面动态而 URL 静态,需实现页面到模板的多对一映射机制。
  3. Native 本地渲染服务:在 Native 侧实现类似 SSR 的本地渲染服务。

端外场景:无法用离线包时,可使用 SSR 替代。另一个预渲染思路是虚拟页 T+2 预渲染——如文档平台展示第 T 页时,预先在可视范围外渲染 T+2 页,点击时直接移动回来。预渲染可能遇到内存问题,可精简预渲染内容(如图片延后获取)。

降级策略

预加载和预渲染需要降级兜底:没有拿到数据时回退 CSR;端外无法使用离线包时用 SSR 替代。