Lazy Loading & Code Splitting

Loading components later with React.lazy, route-based code splitting, using lazy with named exports, and conditional lazy loading -- with simple examples.

Advanced 11 min
TR

Lazy Loading & Code Splitting

Every component we've written so far has been part of the application's FIRST loaded JavaScript bundle. As an application grows, so does this bundle -- users may end up downloading code for pages they'll never even visit. This lesson covers how to avoid that.

Loading a Component Later with React.lazy

lazy() turns a component's code into a SEPARATE file that's downloaded when needed, instead of a regular import:

import { lazy, Suspense, useState } from "react";

// lazy(), component'in KODUNU normal bir import yerine dinamik bir
// import() ile yükler -- bu dosya, uygulamanın ilk yüklenen paketine
// (bundle) DAHİL EDİLMEZ, yalnızca gerçekten gerektiğinde ayrı bir dosya
// olarak indirilir. Bu, "code splitting" dediğimiz şey.
const CourseDetails = lazy(() => import("./CourseDetails.jsx"));

function ReactLazyBasicExample() {
  const [showDetails, setShowDetails] = useState(false);

  return (
    <div>
      <button onClick={() => setShowDetails(true)}>Show Details</button>
      {showDetails && (
        // Suspense, lazy component'in kodu YÜKLENIRKEN gösterilecek bir
        // fallback UI belirtir -- kod indirilene kadar `fallback`
        // gösterilir, indirilince gerçek component render edilir.
        <Suspense fallback={<p>Loading...</p>}>
          <CourseDetails />
        </Suspense>
      )}
    </div>
  );
}

lazy(() => import("./CourseDetails.jsx")) REMOVES CourseDetails's code from the app's initial bundle -- it's only downloaded once showDetails becomes true. Suspense is required to show a fallback during this download (we'll take a closer look at Suspense in the next lesson).

Route-Based Code Splitting

The most common use of lazy() is splitting the pages from the Routing lesson into separate bundles:

import { lazy, Suspense } from "react";
import { BrowserRouter, Routes, Route } from "react-router";

// Routing dersinde her sayfayı normal import ile yüklemiştik -- gerçek bir
// uygulamada, KULLANICI O SAYFAYA GİTMEDEN, hiç ziyaret etmeyeceği
// sayfaların kodunu indirmek istemeyiz. lazy() ile her sayfayı ayrı bir
// paket haline getirip, yalnızca o route'a gidildiğinde indirebiliriz.
const CoursesPage = lazy(() => import("./CoursesPage.jsx"));
const AboutPage = lazy(() => import("./AboutPage.jsx"));

function RouteBasedCodeSplittingExample() {
  return (
    <BrowserRouter>
      {/* Suspense, Routes'un DIŞINA sarmalanıyor -- hangi sayfaya
          gidilirse gidilsin, o sayfanın kodu yüklenene kadar TEK bir
          fallback gösterilir. */}
      <Suspense fallback={<p>Loading page...</p>}>
        <Routes>
          <Route path="/courses" element={<CoursesPage />} />
          <Route path="/about" element={<AboutPage />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

Each page (CoursesPage, AboutPage) is its own separate file -- if a user never visits /about, that page's code is never downloaded. This pattern is called code splitting: breaking an application into multiple small pieces instead of one giant bundle.

Using lazy with Named Exports

lazy() expects import() to resolve to a DEFAULT export -- a component with a named export needs a small adaptation:

import { lazy, Suspense } from "react";

// lazy(), import() fonksiyonunun DEFAULT export döndürmesini bekler --
// eğer CourseChart yalnızca bir named export ise (`export function
// CourseChart() {}`, `export default` DEĞİL), .then() ile onu bir
// "default" alanına sarmalamamız gerekir.
const CourseChart = lazy(() =>
  import("./CourseChart.jsx").then((module) => ({ default: module.CourseChart })),
);

function NamedExportLazyExample() {
  return (
    <Suspense fallback={<p>Loading chart...</p>}>
      <CourseChart />
    </Suspense>
  );
}

.then((module) => ({ default: module.CourseChart })) CONVERTS the named export (CourseChart) into the { default: ... } shape that lazy expects.

Conditional Lazy Loading

lazy() is useful not just for pages, but for ANY rarely-used component:

import { lazy, Suspense, useState } from "react";

// EmojiPicker gibi büyük, nadiren kullanılan bir component'i lazy
// yapmak özellikle faydalı -- kullanıcıların çoğu belki hiç açmaz, o
// zaman kodunu hiç indirmemiş oluruz.
const EmojiPicker = lazy(() => import("./EmojiPicker.jsx"));

function ConditionalLazyLoadExample() {
  const [showPicker, setShowPicker] = useState(false);

  return (
    <div>
      <button onClick={() => setShowPicker(!showPicker)}>
        {showPicker ? "Hide" : "Show"} Emoji Picker
      </button>
      {/* EmojiPicker'ın kodu, `showPicker` İLK KEZ true olana kadar HİÇ
          indirilmez -- yalnızca gerçekten kullanılacaksa yükleniyor. */}
      {showPicker && (
        <Suspense fallback={<p>Loading emoji picker...</p>}>
          <EmojiPicker />
        </Suspense>
      )}
    </div>
  );
}

EmojiPicker's code is never downloaded until the user makes showPicker true for the FIRST time -- most users may never use it, in which case we never download its code at all.

Summary and Glossary

lazy() splits a component's code into a separate file (chunk), downloading it only when it's actually needed -- this REDUCES the amount of JavaScript loaded initially. Its most common uses are splitting pages (routes) or rarely-used components (modals, emoji pickers). lazy() is always used together with Suspense -- a fallback is needed while the code downloads.

Glossary

Code Splitting — The technique of breaking an application's JavaScript into small pieces that are downloaded as needed, instead of one large bundle.

Bundle — An application's JavaScript files combined together to be sent to the browser.

Chunk — A small, separately downloadable JavaScript file produced by code splitting.