home / skills / yanko-belov / code-craft / lazy-loading

lazy-loading skill

/skills/lazy-loading

This skill helps you implement lazy loading to load data on demand, improving initial load times and perceived performance.

npx playbooks add skill yanko-belov/code-craft --skill lazy-loading

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.4 KB
---
name: lazy-loading
description: Use when loading all data upfront. Use when initial page load is slow. Use when fetching data that might not be needed.
---

# 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"

## 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

## 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>
  );
}
```

## Lazy Loading Techniques

### 1. Load on Interaction
```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>
```

### 2. Load on Scroll (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>;
}
```

### 3. Load on Route
```typescript
// Next.js / React Router
const AnalyticsPage = lazy(() => import('./AnalyticsPage'));

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

### 4. Pagination / Infinite Scroll
```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"
**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"
**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"
**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.**

## Quick Reference

| Eager (Usually Bad) | Lazy (Usually Good) |
|---------------------|---------------------|
| Load all on mount | Load visible content first |
| Fetch hidden tab data | Fetch when tab selected |
| Full list at once | Paginate / infinite scroll |
| Below-fold content | Intersection observer |

## Common Rationalizations (All Invalid)

| Excuse | Reality |
|--------|---------|
| "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. |

## 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.

Overview

This skill helps teams avoid loading all data upfront by guiding when and how to fetch only what the user actually needs. It focuses on reducing initial page load time, saving bandwidth and memory, and improving perceived performance. Use it to decide which data is critical at render and which can be deferred.

How this skill works

The skill inspects common eager-loading patterns and recommends replacing broad Promise.all or bulk-fetch strategies with targeted lazy-loading techniques. It describes load-on-interaction, load-on-scroll (Intersection Observer), route-based code/data splitting, and pagination/infinite scroll. It also gives a pressure-resistance protocol to rebut common rationalizations for eager loading.

When to use it

  • Initial page load is slow or exceeds 1–2 seconds
  • Pages that fetch data for hidden tabs, collapsed sections, or below-the-fold content
  • Dashboards or lists that currently load many independent resources in parallel
  • When you fetch large blobs or full histories that are rarely viewed
  • When network or memory usage is a concern on mobile or low-bandwidth users

Best practices

  • Load only immediately visible and critical data on initial render
  • Use useQuery/useLazyQuery patterns to defer noncritical fetches until interaction, mount, or intersection
  • Prefer pagination or infinite scroll over loading full lists at once
  • Parallelize only the small set of critical requests; lazy-load expensive or optional resources
  • Instrument perceived performance and measure load times before and after applying lazy loading

Example use cases

  • Dashboard: fetch profile and preferences on mount; load analytics when analytics tab is selected
  • Notifications: load when header mounts or when user opens the notifications panel
  • History/Activity: load when scrolled into view using an Intersection Observer
  • Analytics page: lazy-load route bundle and data with React Suspense or route-level code splitting
  • Order list: implement pagination or load-more to avoid fetching full history

FAQ

Is parallel fetching always bad?

No. Parallelize the small set of critical requests needed to render the page immediately. Avoid parallelizing optional or expensive requests that the user may never need.

When should I still preload data?

Preload only when you have strong evidence the user will need it imminently (e.g., next expected action) and the cost is low; otherwise prefer on-demand loading to avoid waste.