| title | impact | impactDescription | tags |
|---|---|---|---|
| Barrel File Imports 피하기 | CRITICAL | 200-800ms import 비용, 느린 builds | bundle, 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
| title | impact | impactDescription | tags |
|---|---|---|---|
| 조건부 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 속도를 최적화합니다.
| title | impact | impactDescription | tags |
|---|---|---|---|
| Non-Critical Third-Party Libraries 지연 로드 | MEDIUM | hydration 이후 로드 | 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>
);
}| title | impact | impactDescription | tags |
|---|---|---|---|
| Heavy Components를 위한 Dynamic Imports | CRITICAL | TTI와 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} />;
}| title | impact | impactDescription | tags |
|---|---|---|---|
| 사용자 의도 기반 Preload | MEDIUM | 체감 지연 시간 감소 | 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 속도를 최적화합니다.