$ yh.log
[톺아보기] Vercel의 react-best-practices #4 - Bundle Size Optimization 관련 Rule

[톺아보기] Vercel의 react-best-practices #4 - Bundle Size Optimization 관련 Rule

reactperformancebundle-optimizationcode-splittingnext.js

작성자 : 오예환 | 작성일 : 2026-01-29 | 수정일 : 2026-01-29 | 조회수 :

titleimpactimpactDescriptiontags
Barrel File Imports 피하기CRITICAL200-800ms import 비용, 느린 buildsbundle, imports, tree-shaking, barrel-files, performance

Barrel File Imports 피하기

사용하지 않는 수천 개의 modules를 로드하는 것을 피하기 위해 barrel files 대신 source files에서 직접 import하세요. Barrel files는 여러 modules를 re-export하는 entry points입니다 (예: export * from './module'을 하는 index.js).

인기 있는 icon 및 component libraries는 entry file에 최대 10,000개의 re-exports를 가질 수 있습니다. 많은 React packages의 경우, import하는 데만 200-800ms가 소요되어 개발 속도와 production cold starts 모두에 영향을 미칩니다.

왜 tree-shaking이 도움이 안 되는가: library가 external로 표시되면 (bundled되지 않음), bundler가 최적화할 수 없습니다. tree-shaking을 활성화하기 위해 bundle하면, 전체 module graph를 분석하느라 builds가 상당히 느려집니다.

Incorrect (전체 library를 import함):

import { Check, X, Menu } from "lucide-react";
// 1,583개 modules 로드, dev에서 ~2.8s 추가 소요
// Runtime 비용: 매 cold start마다 200-800ms
 
import { Button, TextField } from "@mui/material";
// 2,225개 modules 로드, dev에서 ~4.2s 추가 소요

Correct (필요한 것만 import함):

import Check from "lucide-react/dist/esm/icons/check";
import X from "lucide-react/dist/esm/icons/x";
import Menu from "lucide-react/dist/esm/icons/menu";
// 3개 modules만 로드 (~1MB 대신 ~2KB)
 
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
// 사용하는 것만 로드

Alternative (Next.js 13.5+):

// next.config.js - optimizePackageImports 사용
module.exports = {
  experimental: {
    optimizePackageImports: ["lucide-react", "@mui/material"],
  },
};
 
// 그러면 편리한 barrel imports를 유지할 수 있음:
import { Check, X, Menu } from "lucide-react";
// build time에 자동으로 direct imports로 변환됨

Direct imports는 15-70% 더 빠른 dev boot, 28% 더 빠른 builds, 40% 더 빠른 cold starts, 그리고 상당히 빠른 HMR을 제공합니다.

흔히 영향받는 Libraries: lucide-react, @mui/material, @mui/icons-material, @tabler/icons-react, react-icons, @headlessui/react, @radix-ui/react-*, lodash, ramda, date-fns, rxjs, react-use.

Reference: How we optimized package imports in Next.js



titleimpactimpactDescriptiontags
조건부 Module 로딩HIGH필요할 때만 대용량 데이터 로드bundle, conditional-loading, lazy-loading

조건부 Module 로딩

기능이 활성화될 때만 대용량 데이터나 modules를 로드하세요.

Example (animation frames lazy-load):

import { useState, useEffect } from "react";
 
// Frame 타입 정의
type Frame = {
  id: string;
  data: ImageData;
  timestamp: number;
};
 
function AnimationPlayer({
  enabled,
  setEnabled,
}: {
  enabled: boolean;
  setEnabled: React.Dispatch<React.SetStateAction<boolean>>;
}) {
  const [frames, setFrames] = useState<Frame[] | null>(null);
 
  useEffect(() => {
    if (enabled && !frames && typeof window !== "undefined") {
      import("./animation-frames.js")
        .then(mod => setFrames(mod.frames))
        .catch(() => setEnabled(false));
    }
  }, [enabled, frames, setEnabled]);
 
  if (!frames) return <Skeleton />;
  return <Canvas frames={frames} />;
}

typeof window !== 'undefined' 체크는 이 module이 SSR용으로 bundling되는 것을 방지하여, server bundle size와 build 속도를 최적화합니다.



titleimpactimpactDescriptiontags
Non-Critical Third-Party Libraries 지연 로드MEDIUMhydration 이후 로드bundle, third-party, analytics, defer

Non-Critical Third-Party Libraries 지연 로드

Analytics, logging, error tracking은 사용자 상호작용을 block하지 않습니다. hydration 이후에 로드하세요.

Incorrect (initial bundle을 block함):

import { Analytics } from "@vercel/analytics/react";
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
      </body>
    </html>
  );
}

Correct (hydration 이후 로드됨):

import dynamic from "next/dynamic";
 
const Analytics = dynamic(() => import("@vercel/analytics/react").then(m => m.Analytics), {
  ssr: false,
});
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
      </body>
    </html>
  );
}


titleimpactimpactDescriptiontags
Heavy Components를 위한 Dynamic ImportsCRITICALTTI와 LCP에 직접 영향bundle, dynamic-import, code-splitting, next-dynamic

Heavy Components를 위한 Dynamic Imports

초기 렌더링에 필요하지 않은 대용량 components를 lazy-load하기 위해 next/dynamic을 사용하세요.

Incorrect (Monaco가 main chunk와 함께 bundle됨 ~300KB):

import { MonacoEditor } from "./monaco-editor";
 
function CodePanel({ code }: { code: string }) {
  return <MonacoEditor value={code} />;
}

Correct (Monaco가 on demand로 로드됨):

import dynamic from "next/dynamic";
 
const MonacoEditor = dynamic(() => import("./monaco-editor").then(m => m.MonacoEditor), {
  ssr: false,
});
 
function CodePanel({ code }: { code: string }) {
  return <MonacoEditor value={code} />;
}


titleimpactimpactDescriptiontags
사용자 의도 기반 PreloadMEDIUM체감 지연 시간 감소bundle, preload, user-intent, hover

사용자 의도 기반 Preload

체감 지연 시간을 줄이기 위해 heavy bundles를 필요하기 전에 preload하세요.

Example (hover/focus 시 preload):

function EditorButton({ onClick }: { onClick: () => void }) {
  const preload = () => {
    if (typeof window !== "undefined") {
      void import("./monaco-editor");
    }
  };
 
  return (
    <button onMouseEnter={preload} onFocus={preload} onClick={onClick}>
      Open Editor
    </button>
  );
}

Example (feature flag 활성화 시 preload):

import { useEffect } from "react";
 
type Props = {
  children: React.ReactNode;
  flags: {
    editorEnabled: boolean;
  };
};
 
function FlagsProvider({ children, flags }: Props) {
  useEffect(() => {
    if (flags.editorEnabled && typeof window !== "undefined") {
      void import("./monaco-editor").then(mod => mod.init());
    }
  }, [flags.editorEnabled]);
 
  return <FlagsContext.Provider value={flags}>{children}</FlagsContext.Provider>;
}

typeof window !== 'undefined' 체크는 preload된 modules가 SSR용으로 bundling되는 것을 방지하여, server bundle size와 build 속도를 최적화합니다.