lazy-loading

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Lazy Loading

懒加载

Overview

概述

Load data when needed, not before. Don't fetch what you might not use.
Upfront loading wastes bandwidth, memory, and time. Load on demand for better performance and user experience.
按需加载数据,而非提前加载。不要获取可能用不到的数据。
提前加载会浪费带宽、内存和时间。按需加载可提升性能与用户体验。

When to Use

适用场景

  • Dashboard loads all data at once
  • Page fetches data for hidden tabs
  • Loading "just in case" data
  • Initial load is slow
  • 仪表板一次性加载所有数据
  • 页面为隐藏标签页获取数据
  • 加载“以防万一”的数据
  • 初始加载速度缓慢

The Iron Rule

铁则

NEVER load data until it's actually needed.
No exceptions:
  • Not for "we might need it"
  • Not for "parallel is faster"
  • Not for "simpler to load everything"
  • Not for "it's not that much data"
NEVER load data until it's actually needed.
无例外:
  • 不要为“可能需要”加载
  • 不要为“并行更快”加载
  • 不要为“一次性加载更简单”加载
  • 不要为“数据量不大”加载

Detection: Eager Overload Smell

检测:过度提前加载迹象

If you load everything upfront, STOP:
typescript
// ❌ VIOLATION: Load everything upfront
async function loadDashboard(userId: string) {
  const [
    profile,
    preferences,
    notifications,    // User might not check
    recentActivity,   // Collapsed by default
    analytics,        // Expensive, rarely viewed
    recommendations,  // Below the fold
    fullHistory       // Paginated anyway
  ] = await Promise.all([
    fetchProfile(userId),
    fetchPreferences(userId),
    fetchNotifications(userId),
    fetchRecentActivity(userId),
    fetchAnalytics(userId),       // Takes 2 seconds!
    fetchRecommendations(userId),
    fetchFullHistory(userId)      // 10MB of data!
  ]);
  
  return { profile, preferences, notifications, ... };
}
Problems:
  • Slowest fetch blocks everything
  • Wastes resources on unused data
  • Poor perceived performance
如果你正在提前加载所有数据,请立即停止:
typescript
// ❌ 违规:一次性加载所有数据
async function loadDashboard(userId: string) {
  const [
    profile,
    preferences,
    notifications,    // 用户可能不会查看
    recentActivity,   // 默认折叠
    analytics,        // 资源消耗大,很少被查看
    recommendations,  // 在可视区域外
    fullHistory       // 本来就支持分页
  ] = await Promise.all([
    fetchProfile(userId),
    fetchPreferences(userId),
    fetchNotifications(userId),
    fetchRecentActivity(userId),
    fetchAnalytics(userId),       // 耗时2秒!
    fetchRecommendations(userId),
    fetchFullHistory(userId)      // 10MB数据!
  ]);
  
  return { profile, preferences, notifications, ... };
}
问题:
  • 最慢的请求会阻塞所有操作
  • 为未使用的数据浪费资源
  • 感知性能差

The Correct Pattern: Load On Demand

正确模式:按需加载

typescript
// ✅ CORRECT: Load critical data first, rest on demand

// Initial load - only what's immediately visible
async function loadDashboard(userId: string) {
  const [profile, preferences] = await Promise.all([
    fetchProfile(userId),
    fetchPreferences(userId)
  ]);
  
  return { profile, preferences };
}

// React component with lazy loading
function Dashboard({ userId }) {
  // Critical data loaded immediately
  const { profile, preferences } = useInitialData(userId);
  
  // Notifications: load when header mounts
  const notifications = useLazyQuery(
    () => fetchNotifications(userId),
    { loadOn: 'mount' }
  );
  
  // Analytics: load when tab is selected
  const [analyticsTab, setAnalyticsTab] = useState(false);
  const analytics = useLazyQuery(
    () => fetchAnalytics(userId),
    { loadOn: analyticsTab }
  );
  
  // History: load when scrolled into view
  const historyRef = useRef();
  const history = useLazyQuery(
    () => fetchHistory(userId),
    { loadOn: useIntersectionObserver(historyRef) }
  );
  
  return (
    <div>
      <Header profile={profile} notifications={notifications} />
      <Tabs>
        <Tab label="Overview">...</Tab>
        <Tab label="Analytics" onSelect={() => setAnalyticsTab(true)}>
          {analytics.loading ? <Skeleton /> : <AnalyticsChart data={analytics.data} />}
        </Tab>
      </Tabs>
      <div ref={historyRef}>
        {history.data && <HistoryList items={history.data} />}
      </div>
    </div>
  );
}
typescript
// ✅ 正确:先加载关键数据,其余按需加载

// 初始加载 - 仅加载立即可见的数据
async function loadDashboard(userId: string) {
  const [profile, preferences] = await Promise.all([
    fetchProfile(userId),
    fetchPreferences(userId)
  ]);
  
  return { profile, preferences };
}

// 带有懒加载的React组件
function Dashboard({ userId }) {
  // 关键数据立即加载
  const { profile, preferences } = useInitialData(userId);
  
  // 通知:在头部组件挂载时加载
  const notifications = useLazyQuery(
    () => fetchNotifications(userId),
    { loadOn: 'mount' }
  );
  
  // 分析数据:在选中对应标签页时加载
  const [analyticsTab, setAnalyticsTab] = useState(false);
  const analytics = useLazyQuery(
    () => fetchAnalytics(userId),
    { loadOn: analyticsTab }
  );
  
  // 历史记录:在滚动至可视区域时加载
  const historyRef = useRef();
  const history = useLazyQuery(
    () => fetchHistory(userId),
    { loadOn: useIntersectionObserver(historyRef) }
  );
  
  return (
    <div>
      <Header profile={profile} notifications={notifications} />
      <Tabs>
        <Tab label="Overview">...</Tab>
        <Tab label="Analytics" onSelect={() => setAnalyticsTab(true)}>
          {analytics.loading ? <Skeleton /> : <AnalyticsChart data={analytics.data} />}
        </Tab>
      </Tabs>
      <div ref={historyRef}>
        {history.data && <HistoryList items={history.data} />}
      </div>
    </div>
  );
}

Lazy Loading Techniques

懒加载技巧

1. Load on Interaction

1. 交互时加载

typescript
// Load when user clicks tab
const [showDetails, setShowDetails] = useState(false);
const details = useQuery(fetchDetails, { enabled: showDetails });

<Tab onClick={() => setShowDetails(true)}>
  {details.data ?? <Skeleton />}
</Tab>
typescript
// 用户点击标签页时加载
const [showDetails, setShowDetails] = useState(false);
const details = useQuery(fetchDetails, { enabled: showDetails });

<Tab onClick={() => setShowDetails(true)}>
  {details.data ?? <Skeleton />}
</Tab>

2. Load on Scroll (Intersection Observer)

2. 滚动时加载(Intersection Observer)

typescript
function LazySection({ loadFn, children }) {
  const ref = useRef();
  const [loaded, setLoaded] = useState(false);
  const data = useQuery(loadFn, { enabled: loaded });
  
  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => entry.isIntersecting && setLoaded(true)
    );
    observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);
  
  return <div ref={ref}>{data.data ? children(data.data) : <Skeleton />}</div>;
}
typescript
function LazySection({ loadFn, children }) {
  const ref = useRef();
  const [loaded, setLoaded] = useState(false);
  const data = useQuery(loadFn, { enabled: loaded });
  
  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => entry.isIntersecting && setLoaded(true)
    );
    observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);
  
  return <div ref={ref}>{data.data ? children(data.data) : <Skeleton />}</div>;
}

3. Load on Route

3. 路由切换时加载

typescript
// Next.js / React Router
const AnalyticsPage = lazy(() => import('./AnalyticsPage'));

<Route path="/analytics" element={
  <Suspense fallback={<Loading />}>
    <AnalyticsPage />
  </Suspense>
} />
typescript
// Next.js / React Router
const AnalyticsPage = lazy(() => import('./AnalyticsPage'));

<Route path="/analytics" element={
  <Suspense fallback={<Loading />}>
    <AnalyticsPage />
  </Suspense>
} />

4. Pagination / Infinite Scroll

4. 分页 / 无限滚动

typescript
function OrderList() {
  const [page, setPage] = useState(1);
  const { data, hasMore } = useOrders({ page, limit: 20 });
  
  return (
    <>
      {data.map(order => <OrderRow key={order.id} order={order} />)}
      {hasMore && <button onClick={() => setPage(p => p + 1)}>Load More</button>}
    </>
  );
}
typescript
function OrderList() {
  const [page, setPage] = useState(1);
  const { data, hasMore } = useOrders({ page, limit: 20 });
  
  return (
    <>
      {data.map(order => <OrderRow key={order.id} order={order} />)}
      {hasMore && <button onClick={() => setPage(p => p + 1)}>Load More</button>}
    </>
  );
}

Pressure Resistance Protocol

应对质疑指南

1. "We Might Need It"

1. “我们可能需要它”

Pressure: "Load it now in case user needs it"
Response: "Might" means probably won't. Load when they actually need it.
Action: Load on demand, not on speculation.
质疑:“现在加载,以防用户需要”
回应:“可能”意味着大概率不需要。等用户真正需要时再加载。
**行动:**按需加载,而非推测性加载。

2. "Parallel Is Faster"

2. “并行加载更快”

Pressure: "Loading everything in parallel is faster than sequential"
Response: Parallel loading of unneeded data is slower than not loading it.
Action: Parallelize what you need. Lazy load what you might need.
质疑:“一次性并行加载所有数据比顺序加载更快”
**回应:**并行加载不需要的数据,比不加载这些数据要慢。
**行动:**并行加载需要的数据,懒加载可能需要的数据。

3. "Simpler to Load Everything"

3. “一次性加载所有数据更简单”

Pressure: "One fetch function is simpler"
Response: Simple code that's slow and wasteful isn't simple.
Action: Structured lazy loading is maintainable and performant.
质疑:“一个请求函数更简单”
**回应:**缓慢且浪费资源的“简单”代码并非真正的简单。
**行动:**结构化的懒加载既易于维护又性能优异。

Red Flags - STOP and Reconsider

危险信号 - 立即停止并重新考虑

  • Promise.all
    with 5+ fetches on page load
  • Fetching data for collapsed/hidden sections
  • Loading full lists instead of paginating
  • Slow initial page loads
  • "Loading..." takes more than 1-2 seconds
All of these mean: Implement lazy loading.
  • 页面加载时使用
    Promise.all
    执行5个以上请求
  • 为折叠/隐藏的区域获取数据
  • 加载完整列表而非分页
  • 初始页面加载缓慢
  • “加载中...”耗时超过1-2秒
以上所有情况都意味着:请实现懒加载。

Quick Reference

快速参考

Eager (Usually Bad)Lazy (Usually Good)
Load all on mountLoad visible content first
Fetch hidden tab dataFetch when tab selected
Full list at oncePaginate / infinite scroll
Below-fold contentIntersection observer
提前加载(通常不佳)懒加载(通常更佳)
挂载时加载所有内容先加载可视内容
获取隐藏标签页的数据选中标签页时获取
一次性加载完整列表分页 / 无限滚动
可视区域外的内容使用Intersection Observer

Common Rationalizations (All Invalid)

常见合理化借口(均不成立)

ExcuseReality
"Might need it"Load when you do need it.
"Parallel is faster"Not loading is fastest.
"Simpler"Slow isn't simple.
"It's not much data"It adds up. Bandwidth costs.
"Better UX to have it ready"Slow load is worse UX.
借口事实
“可能需要它”需要时再加载。
“并行加载更快”不加载才是最快的。
“更简单”缓慢的代码并非简单。
“数据量不大”积少成多,还会产生带宽成本。
“提前加载好能提升UX”缓慢的加载体验更糟糕。

The Bottom Line

总结

Load what's visible. Defer the rest. Paginate large lists.
Initial load = critical data only. Everything else loads on interaction, scroll, or navigation. Users shouldn't wait for data they won't see.
加载可视内容,延迟加载其余内容,对大型列表进行分页。
初始加载仅包含关键数据。其余所有内容在交互、滚动或导航时加载。用户不应该为他们看不到的数据等待。