前端懒加载全家桶:图片、路由、组件的按需加载

首屏 6 秒变 1.5 秒,核心武器正是懒加载。

用户打开一个页面,只看到首屏那一小块区域。而图片、JS、组件,你通常一次性全加载了。那些藏在折叠线下面的内容,那些用户根本不会滚动到的代码,都在跟首屏抢带宽。

懒加载不是"延迟加载",而是只加载用户当前需要的东西。本文从图片、路由、组件三个维度,给出可直接落地的方案。

一、图片懒加载:最直观的性能提升

1. 原生 loading="lazy"(推荐)

现代浏览器原生支持,一行代码搞定:

html 复制代码
<img src="hero.jpg" loading="lazy" alt="产品图" />

浏览器会自动判断图片是否接近可视区域,接近时才开始加载。Chrome 77+、Edge 79+、Firefox 75+ 均支持。兼容老旧浏览器时,作为渐进增强的手段,不用担心不支持的浏览器会崩溃,它们只会忽略这个属性并立即加载图片。

2. IntersectionObserver 手动实现

需要更细粒度的控制(如自定义加载阈值、动态背景图、懒加载视频)时,可用 IntersectionObserver

javascript 复制代码
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;          // 将真实图片地址赋给 src
      observer.unobserve(img);            // 加载完成后停止观察
    }
  });
}, { rootMargin: '200px' });             // 提前 200px 开始加载,用户无感知

document.querySelectorAll('img[data-src]').forEach(img => {
  observer.observe(img);
});

对应 HTML:

html 复制代码
<img data-src="large-photo.jpg" src="placeholder.png" alt="产品图" />

占位图 placeholder.png 可以是极小的灰色方块或模糊缩略图,保证页面布局稳定。

3. 避免布局偏移(CLS)

懒加载最大的隐患是图片加载后撑开页面导致内容跳动。修复方式很简单------给图片容器预留占位空间:

css 复制代码
.img-wrapper {
  position: relative;
  width: 100%;
  padding-bottom: 56.25%; /* 16:9 比例 */
}
.img-wrapper img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}

或者在 <img> 上直接设置 widthheight 属性,现代浏览器会根据这两个属性自动计算宽高比并保留空间。

二、路由懒加载:SPA 首屏体积的"瘦身刀"

单页应用首屏加载慢,很大程度是因为 import 会把所有页面组件打包进一个 bundle。通过动态 import() 实现代码分割,让用户访问哪个页面时才加载哪个页面对应的 JS。

1. React Router v6

tsx 复制代码
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));

function App() {
  return (
    <Suspense fallback={<Spin size="large" />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/analytics" element={<Analytics />} />
      </Routes>
    </Suspense>
  );
}

React.lazy 配合 Suspense,在组件加载期间展示 fallback(骨架屏或 loading 动画)。Webpack 和 Vite 在构建时识别到 import(),自动将这三个页面拆成独立的 chunk。首屏 bundle 体积通常能下降 40%-60%。

2. Vue Router

javascript 复制代码
const Dashboard = () => import('./views/Dashboard.vue');
const Settings = () => import('./views/Settings.vue');

const router = createRouter({
  routes: [
    { path: '/dashboard', component: Dashboard },
    { path: '/settings', component: Settings },
  ]
});

Vue 会自动将每个动态导入的组件拆成独立 chunk,无需额外配置。

3. 路由懒加载与预加载的配合

懒加载的副作用是切换路由时有短暂的加载延迟。对于用户极大概率会访问的页面(比如首页→列表页),可在当前页面空闲时预加载下一个页面的 chunk:

tsx 复制代码
// 在 Dashboard 页面中,预加载 Settings 页面
const Settings = lazy(() => import('./pages/Settings'));

function Dashboard() {
  useEffect(() => {
    // 页面渲染完成后,浏览器空闲时预加载
    const preload = () => import('./pages/Settings');
    if ('requestIdleCallback' in window) {
      requestIdleCallback(preload);
    } else {
      setTimeout(preload, 1000);
    }
  }, []);
}

或者在 <link> 标签中声明:

html 复制代码
<link rel="prefetch" href="/assets/Settings.chunk.js" as="script">

Webpack 魔法注释同样可以触发 prefetch:

javascript 复制代码
const Settings = lazy(() => import(/* webpackPrefetch: true */ './pages/Settings'));

三、组件懒加载:非首屏可见的重型组件"延迟到场"

页面中有些组件体积庞大(图表、富文本编辑器、视频播放器、地图),但用户不一定滚动到它们所在的位置。

1. React 示例:图表组件懒加载

tsx 复制代码
import { lazy, Suspense, useState } from 'react';
import { useInView } from 'react-intersection-observer';

const HeavyChart = lazy(() => import('./HeavyChart'));

function Dashboard() {
  const { ref, inView } = useInView({ triggerOnce: true, rootMargin: '200px' });
  const [showChart, setShowChart] = useState(false);

  useEffect(() => {
    if (inView && !showChart) setShowChart(true);
  }, [inView]);

  return (
    <div ref={ref} style={{ minHeight: 400 }}>
      {showChart ? (
        <Suspense fallback={<div style={{ height: 400, background: '#f0f0f0' }}>加载图表中...</div>}>
          <HeavyChart />
        </Suspense>
      ) : (
        <div style={{ height: 400, background: '#f0f0f0' }}>滚动至此加载图表</div>
      )}
    </div>
  );
}

关键点:

  • useInView(来自 react-intersection-observer)监听组件是否进入视口。
  • triggerOnce: true 保证只触发一次,加载后不再卸载。
  • rootMargin: '200px' 提前加载,用户无感知。
  • 占位 div 预留高度,防止布局跳动。

2. Vue 示例:地图组件懒加载

vue 复制代码
<template>
  <div ref="container" style="min-height: 400px;">
    <div v-if="!loaded" style="height: 400px; background: #f0f0f0;">
      滚动至此加载地图
    </div>
    <MapComponent v-if="loaded" />
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const loaded = ref(false);
const container = ref(null);

onMounted(() => {
  const observer = new IntersectionObserver(
    ([entry]) => {
      if (entry.isIntersecting) {
        loaded.value = true;
        observer.disconnect();
      }
    },
    { rootMargin: '200px' }
  );
  observer.observe(container.value);
});
</script>

3. 哪些组件应该懒加载?

  • 图表库(ECharts、Highcharts 等)
  • 富文本编辑器(Quill、TinyMCE、Monaco Editor)
  • 地图 SDK(高德、百度、Mapbox)
  • 视频/音频播放器
  • 复杂表单(多步骤向导)

四、三种懒加载的配合:一个完整页面的加载策略

假设一个后台管理首页包含:

  • 顶部数据卡片(首屏可见)
  • 折线图(第二屏)
  • 用户列表(第三屏,3000+ 数据)

完整加载策略如下:

css 复制代码
页面打开(首屏)
  ├─ 数据卡片组件 ------ 立即渲染
  ├─ 图表占位 div ------ 展示骨架屏(400px 灰色区块)
  ├─ 列表占位 div ------ 展示骨架屏(600px 灰色区块)
  ├─ 底部图片 ------ 不加载,保持空白

用户开始滚动 ↓

  图表区域进入视口
  → 动态加载 echarts 并渲染图表

  列表区域进入视口
  → 动态加载列表组件 + 只渲染可视行(虚拟滚动)

  底部图片进入视口(提前 200px)
  → 将 data-src 替换为真实图片地址

用户感知到的首屏时间大幅缩短,而后续内容的加载在滚动中自然完成。

五、懒加载常见踩坑与解决方案

1. SEO 问题

搜索引擎爬虫通常不会执行 JS,懒加载的图片和内容可能无法被索引。解决 :首屏关键图片不使用懒加载;重要页面考虑 SSR/SSG,或为图片添加 <noscript> 降级。

2. 打印时图片缺失

用户打印页面时,懒加载的图片可能还没加载。解决 :在 @media print 中强制显示所有图片。

3. 用户快速滚动时的加载延迟

如果用户快速滚动到底部,多个懒加载组件同时触发,可能导致短时间大量请求。解决 :使用 IntersectionObserverrootMargin 提前触发,配合请求优先级控制。

4. 骨架屏与 CLS

懒加载组件在加载期间如果没有占位空间,会导致后续内容频繁跳动,累积布局偏移(CLS)扣分。解决:始终为懒加载的组件预留最小高度,使用骨架屏或灰色占位块。

六、总结

类型 核心 API 适用场景
图片懒加载 loading="lazy"IntersectionObserver 所有非首屏图片
路由懒加载 React.lazy / dynamic import SPA 的各页面
组件懒加载 IntersectionObserver + lazy 图表、编辑器、地图等重型组件

懒加载的本质不是"延迟",而是用空间换时间 ------在关键路径上削减一切非必要资源,让用户更快看到有意义的内容,然后悄悄地把其他东西加载好。你不需要一次性把所有优化手段都加上,但至少从今天开始,每写一个 <img> 就多想一秒:"这个图片,用户现在真的需要吗?"

相关推荐
千帆_Evan1 小时前
🤖 AI 写代码总在重复造轮子?我们把 Utils 复用门禁做进了 Agent 运行时(已开源)
前端·agent
Kel1 小时前
多 Tab 聊天不翻车:纯前端实现跨标签页消息同步的三层协同架构
前端·javascript·人工智能
半个落月1 小时前
Promise 入门:理解 async/await 与 Promise.all
前端·javascript
wear工程师1 小时前
synchronized 锁升级:多数人还在背 JDK8 的答案,偏向锁早在 JDK18 就没了
java·面试
廖菌1 小时前
DATASPACE3D 产品介绍:面向数字孪生与 Web3D 场景的一体化可视化搭建平台
前端·webgl
敖行客 Allthinker1 小时前
前端调试实战:DevTools调试工具 + 二分定位Bug法全总结
前端·typescript·chrome devtools
暗不需求1 小时前
手写深拷贝:从 JSON 到递归,彻底解决循环引用问题
前端·面试
灏仟亿前端技术团队1 小时前
DOMagic:把 AI 修改绑定到页面节点的前端开发态插件
前端