Skip to content

🚀 웹 앱 번들 사이즈 최적화 (198kB → 160kB)#416

Merged
manNomi merged 10 commits into
mainfrom
optimize/bundle-size
Feb 14, 2026
Merged

🚀 웹 앱 번들 사이즈 최적화 (198kB → 160kB)#416
manNomi merged 10 commits into
mainfrom
optimize/bundle-size

Conversation

@manNomi

@manNomi manNomi commented Feb 8, 2026

Copy link
Copy Markdown
Contributor

Summary

웹 앱의 First Load JS shared by all을 198 kB → 160 kB (-38 kB, -19.2%)로 감소시킵니다.

변경 사항

1. Sentry transpileClientSDK 비활성화

  • IE11 호환을 위한 SDK 트랜스파일을 제거하여 번들 크기 감소
  • 현대 브라우저만 지원하므로 IE11 호환 불필요

2. Sentry replayIntegration lazy load

  • Session Replay SDK (~30-40kB)를 초기 번들에서 제외
  • lazyLoadIntegration으로 비동기 로딩하여 초기 로딩 성능 개선
  • Replay 기능은 동일하게 동작 (로딩 시점만 지연)

3. @next/bundle-analyzer 추가

  • 번들 분석 도구 설치 (ANALYZE=true pnpm --filter @solid-connect/web run analyze)
  • 향후 번들 사이즈 모니터링 가능

4. optimizePackageImports 설정

  • lucide-react, @radix-ui/*, zod, react-hook-form 등 tree-shaking 최적화
  • 사용하지 않는 모듈이 번들에 포함되지 않도록 설정

5. score/search 페이지 dynamic import

  • ScoreScreen, SearchBar, SchoolSearchForm 컴포넌트를 next/dynamic으로 변경
  • 해당 페이지 진입 시에만 클라이언트 코드 로딩

빌드 결과 비교

항목 Before After 변화
First Load JS shared 198 kB 160 kB -38 kB (-19.2%)
/university/score 250 kB 212 kB -38 kB
/university/score/submit/gpa 321 kB 284 kB -37 kB
/university/score/submit/language-test 322 kB 285 kB -37 kB
/university/search 285 kB 248 kB -37 kB
Shared chunk count 4 3 -1 (d763957a 제거됨)

참고

  • useMutationWithFailure.ts의 기존 타입 에러가 main에도 존재합니다 (이 PR과 무관)

@vercel

vercel Bot commented Feb 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
solid-connect-web-admin Error Error Feb 14, 2026 10:03am
solid-connection-web Ready Ready Preview, Comment Feb 14, 2026 10:03am

@coderabbitai

coderabbitai Bot commented Feb 8, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@manNomi has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 44 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

  1. 포맷팅 및 들여쓰기 정규화가 여러 파일에서 적용되었습니다.
  2. 두 Score 테이블(GpaScoreTable, LanguageScoreTable)에 fetchScoresuseCallback으로 메모이제이션하고 useEffect 의존성을 안정화했습니다.
  3. Button 및 Input 컴포넌트에 명시적 named export가 추가되었습니다.
  4. 일부 페이지 컴포넌트를 next/dynamic으로 클라이언트 전용 동적 로드하도록 변경했습니다.
  5. Next.js에 번들 분석기(@next/bundle-analyzer)와 optimizePackageImports 설정을 추가했습니다.
  6. Sentry 리플레이 통합을 런타임 lazy-load 방식으로 전환했습니다.
  7. useMutationWithFailure.ts 파일을 추가해 React Query의 useMutation을 onFailure를 지원하도록 확장했습니다.
  8. .npmrc와 CI용 tsconfig.ci.json 등 구성 파일이 추가·수정되었습니다.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Suggested reviewers

  • wibaek
  • enunsnv
  • khwww
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 웹 앱 번들 사이즈 최적화의 핵심 변화(198kB → 160kB)를 명확하고 간결하게 설명하며, 실제 변경사항의 주요 목표를 정확히 반영하고 있습니다.
Description check ✅ Passed PR 설명이 정해진 템플릿(관련 이슈, 작업 내용, 특이 사항, 리뷰 요구사항)을 완벽하게 따르지는 않으나, Summary, 변경 사항, 빌드 결과 비교 등으로 구성되어 매우 상세하고 명확합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch optimize/bundle-size

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@apps/admin/src/components/features/scores/GpaScoreTable.tsx`:
- Around line 214-223: Fix the inconsistent indentation in the pagination block
inside GpaScoreTable: align the Array.from({ length: totalPages }, ...)
expression to the same indentation level as the surrounding <Button> siblings
and make the props on the Button (key, onClick, variant) vertically aligned
(ensure onClick is at the same indent level as key and variant). Locate the
pagination map using Array.from and the Button elements that call
handlePageChange(idx + 1) and reference page to set the variant, then reformat
those lines so indentation is consistent and symmetric.

In `@apps/admin/src/components/features/scores/LanguageScoreTable.tsx`:
- Around line 236-245: Fix the indentation in LanguageScoreTable's pagination
block to match GpaScoreTable: align the {Array.from({ length: totalPages }, (_,
idx) => ( ... ))} block with the surrounding <Button> container and ensure the
attributes inside each Button (key={idx + 1}, onClick={() =>
handlePageChange(idx + 1)}, variant={page === idx + 1 ? "default" :
"secondary"}) have consistent indentation levels (make onClick aligned with key
and variant). Locate the pagination code in LanguageScoreTable (references:
totalPages, page, handlePageChange) and reformat it so its JSX indentation
mirrors the GpaScoreTable pagination section.

In `@apps/admin/src/routes/auth/login.tsx`:
- Around line 56-86: The JSX indentation inside the login form is inconsistent:
ensure each <div className="space-y-1"> is indented as a child of the
surrounding <form>, align all props of the Input components (id, type,
placeholder, value, onChange, disabled, required, className) at the same
indentation level, and align the closing `/>` for each <Input> and the closing
`</div>` tags to match their opening tags; after fixing the whitespace, run
"biome format --write" to normalize formatting across the <Label>, <Input>, and
surrounding form markup (refer to the Input and Label usage in this login
component).

In `@apps/web/next.config.mjs`:
- Around line 97-98: Remove the unsupported transpileClientSDK option from
next.config.mjs (delete the transpileClientSDK: false line and its IE11-related
comment); reference the `@sentry/nextjs` change (v8+ removed this option) and, if
IE11/older-browser support is required, instruct to implement explicit
transpilation and polyfills via Next.js webpack/babel config instead of using
transpileClientSDK.

In `@apps/web/package.json`:
- Line 50: The dependency `@next/bundle-analyzer` in apps/web package.json is
pinned to ^16.1.6 which mismatches the project's Next.js version (^14.2.35);
update the "@next/bundle-analyzer" entry to a 14.x compatible version (e.g.
^14.0.0 or the specific v14.x that matches Next 14) so the `@next/`* major
versions align, then reinstall deps (npm/yarn/pnpm install) and run the
app/build to verify compatibility; locate the "@next/bundle-analyzer" entry in
package.json to make this change.
🧹 Nitpick comments (7)
apps/web/src/components/ui/FloatingUpBtn/hooks/useFloatingUpHandler.ts (1)

10-15: handleClickuseCallback으로 감싸는 것을 고려해볼 수 있습니다.

handleScroll은 메모이제이션했지만, handleClick은 매 렌더마다 새로 생성됩니다. 이 함수가 자식 컴포넌트에 prop으로 전달된다면 불필요한 리렌더를 유발할 수 있습니다. 다만 현재 사용 패턴에서 문제가 없다면 무시하셔도 괜찮습니다.

♻️ 선택적 개선안
-  const handleClick = () => {
-    window.scrollTo({
-      top: 0,
-      behavior: "smooth",
-    });
-  };
+  const handleClick = useCallback(() => {
+    window.scrollTo({
+      top: 0,
+      behavior: "smooth",
+    });
+  }, []);
apps/web/src/app/university/score/page.tsx (1)

6-6: loading 폴백을 추가하면 UX가 더 좋아질 수 있어요.

ssr: false로 동적 임포트하면 JS 청크가 로드될 때까지 해당 영역이 비어 보입니다. 간단한 스켈레톤이나 스피너를 loading 옵션에 넣어주면 사용자 체감이 자연스러워집니다.

💡 예시
-const ScoreScreen = dynamic(() => import("./ScoreScreen"), { ssr: false });
+const ScoreScreen = dynamic(() => import("./ScoreScreen"), {
+  ssr: false,
+  loading: () => <div className="flex h-40 items-center justify-center">로딩 중...</div>,
+});
apps/web/src/app/university/search/page.tsx (1)

6-7: 동적 임포트에 loading 폴백이 없어요.

  1. SearchBarSchoolSearchForm 모두 ssr: false로 설정되어 있어서, JS가 로드되기 전까지 해당 영역이 비어 보일 수 있습니다.
  2. dynamicloading 옵션으로 간단한 스켈레톤이나 스피너를 넣어주면 사용자 체감 로딩 속도가 개선됩니다.
next/dynamic loading option usage
💡 예시
-const SearchBar = dynamic(() => import("./SearchBar"), { ssr: false });
-const SchoolSearchForm = dynamic(() => import("./PageContent"), { ssr: false });
+const SearchBar = dynamic(() => import("./SearchBar"), {
+  ssr: false,
+  loading: () => <div className="h-10 animate-pulse rounded-lg bg-gray-100" />,
+});
+const SchoolSearchForm = dynamic(() => import("./PageContent"), {
+  ssr: false,
+  loading: () => <div className="h-40 animate-pulse rounded-lg bg-gray-100" />,
+});
apps/admin/src/lib/api/scores.ts (1)

30-44: 어학 성적 조회 및 수정 API 로직 확인 완료

포맷팅 정리가 잘 되었습니다. 다만 향후 타입 안전성 개선을 고려해볼 수 있는 부분이 있습니다.

Line 38의 as LanguageTestType 타입 단언은 서버에서 예상치 못한 값이 올 경우 런타임 오류를 발생시킬 수 있습니다. 필요시 타입 가드를 통한 검증을 추가하는 것을 고려해보세요.

apps/web/src/lib/react-query/useMutationWithFailure.ts (2)

77-99: mutate/mutateAsync가 매 렌더마다 새 참조를 생성합니다.

  1. mutatemutateAsyncuseCallback 없이 매 렌더시 재생성됩니다.
  2. React Query 자체의 mutation.mutate도 안정적 참조가 아니므로 실질적 문제는 아니지만, 이 함수들을 자식 컴포넌트의 prop으로 전달하는 경우 불필요한 리렌더가 발생할 수 있습니다.
  3. 현재 규모에서는 큰 영향이 없으므로, 필요해지면 useCallback으로 감싸는 것을 고려해 주세요.

43-62: normalizeMutationOptions / normalizeMutateOptions — 타입 캐스팅에 대한 메모.

  1. Line 49, 60에서 as 캐스팅을 사용하는데, 이는 mergeOnErrorTDataunknown으로 처리하기 때문에 필요합니다.
  2. 안전성 측면에서 문제는 없지만, mergeOnError의 제네릭에 TData를 추가하면 캐스팅 없이 해결할 수 있습니다.
♻️ 캐스팅 제거를 위한 제안
-const mergeOnError = <TError, TVariables, TContext>(
-  onError?: UseMutationOptions<unknown, TError, TVariables, TContext>["onError"],
+const mergeOnError = <TData, TError, TVariables, TContext>(
+  onError?: UseMutationOptions<TData, TError, TVariables, TContext>["onError"],
   onFailure?: (error: TError, variables: TVariables, context: TContext | undefined) => void,
-): UseMutationOptions<unknown, TError, TVariables, TContext>["onError"] => {
+): UseMutationOptions<TData, TError, TVariables, TContext>["onError"] => {
   if (!onError && !onFailure) return undefined;
-  return (...args: Parameters<NonNullable<UseMutationOptions<unknown, TError, TVariables, TContext>["onError"]>>) => {
+  return (...args: Parameters<NonNullable<UseMutationOptions<TData, TError, TVariables, TContext>["onError"]>>) => {
     onError?.(...args);
     onFailure?.(args[0], args[1], args[2]);
   };
 };

이렇게 하면 normalizeMutationOptionsnormalizeMutateOptions 내부의 as 캐스팅을 제거할 수 있습니다.

apps/web/sentry.client.config.ts (1)

35-44: lazyLoadIntegration Promise에 에러 처리가 없습니다.

네트워크 실패 등으로 lazy load가 실패할 경우 unhandled promise rejection이 발생할 수 있어요. Session Replay는 비필수 기능이니 조용히 실패해도 괜찮지만, 명시적으로 .catch()를 추가해 주는 것이 안전합니다.

🛡️ 에러 처리 추가 제안
   Sentry.lazyLoadIntegration("replayIntegration").then((replay) => {
     Sentry.addIntegration(
       replay({
         maskAllText: true,
         blockAllMedia: true,
       }),
     );
-  });
+  }).catch(() => {
+    // Session Replay lazy load 실패 시 무시 (비필수 기능)
+  });

Comment thread apps/admin/src/components/features/scores/GpaScoreTable.tsx Outdated
Comment thread apps/admin/src/components/features/scores/LanguageScoreTable.tsx Outdated
Comment thread apps/admin/src/routes/auth/login.tsx Outdated
Comment thread apps/web/next.config.mjs
Comment on lines +97 to +98
// IE11 지원 불필요 - 번들 사이즈 최적화를 위해 비활성화
transpileClientSDK: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

@sentry/nextjs v10 transpileClientSDK option documentation

💡 Result:

transpileClientSDK is not available in @sentry/nextjs v10 (or any v8+). It was removed in @sentry/nextjs 8.0.0 when Sentry dropped support for IE11/older browsers; the release notes explicitly say the option was removed and that you must down-compile the SDK yourself via your Next.js/Webpack setup if you still need that compatibility. [1]

Historically (pre-v8), transpileClientSDK: true told the Next.js SDK’s Webpack config to transpile Sentry’s client bundle to match your app’s transpilation level (to support older browsers). [2]

If your goal is “support older browsers,” note that current Sentry JavaScript SDKs target modern syntax (ES2020) and Sentry’s docs recommend transpiling + polyfills on your side if you need earlier browser versions. [3]

Sources: [1] [2] [3]


🌐 Web query:

Sentry SDK transpileClientSDK option removed deprecated v8 v9 v10

💡 Result:

  • transpileClientSDK was added to the Sentry Next.js SDK in the v7 line (to force Webpack to transpile Sentry’s client bundle for older browsers like IE11). (newreleases.io)
  • It was removed in @sentry/nextjs 8.0.0 (Sentry JS SDK v8) because v8 dropped support for IE11 / older browsers, so the SDK no longer provides this built-in transpilation switch. (newreleases.io)
  • Therefore, the option is not available in v8, v9, or v10 of @sentry/nextjs. If you still need older-browser support, you must configure Next.js/Webpack to down-compile/transpile the Sentry packages yourself. (newreleases.io)

Citations:


🌐 Web query:

@sentry/nextjs changelog transpileClientSDK

💡 Result:

transpileClientSDK was added to @sentry/nextjs in sentry-javascript v7.8.0 (“fix(nextjs): Add transpileClientSDK option”, PR #5472) as a Next.js Webpack config flag. It makes Next/Webpack transpile the Sentry client SDK code using the same transpilation rules as your app, which helps when targeting older browsers (e.g., missing ES6/ES6+ features like object spread). [1]

In the @sentry/nextjs type definitions (v7.x), it’s documented as:

  • transpileClientSDK?: boolean; — “apply the same transpilation rules to the SDK code as apply to user code…” [2]

Sources: [1] [2]


transpileClientSDK 옵션은 @sentry/nextjs v10에서 지원되지 않으므로 제거가 필요합니다.

다음과 같은 변경 사항이 필요합니다:

  1. 옵션 제거
    @sentry/nextjs 8.0.0부터 transpileClientSDK 옵션이 완전히 제거되었으므로, 이 라인은 삭제하세요. 현재 설정은 v10에서 인식되지 않아 자동으로 무시됩니다.

  2. IE11 지원 재평가
    Sentry JavaScript SDK v8+ 이후 IE11 지원이 공식적으로 중단되었습니다. 만약 이전 브라우저 지원이 필수라면, Next.js의 webpack 설정을 통해 직접 transpilation + polyfill을 구성해야 합니다. 현재 주석의 의도(번들 최적화)라면 주석도 함께 제거하세요.

🤖 Prompt for AI Agents
In `@apps/web/next.config.mjs` around lines 97 - 98, Remove the unsupported
transpileClientSDK option from next.config.mjs (delete the transpileClientSDK:
false line and its IE11-related comment); reference the `@sentry/nextjs` change
(v8+ removed this option) and, if IE11/older-browser support is required,
instruct to implement explicit transpilation and polyfills via Next.js
webpack/babel config instead of using transpileClientSDK.

Comment thread apps/web/package.json
"zustand": "^5.0.7"
},
"devDependencies": {
"@next/bundle-analyzer": "^16.1.6",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat apps/web/package.json | grep -A 100 '"dependencies"' | head -30

Repository: solid-connection/solid-connect-web

Length of output: 1085


🏁 Script executed:

cat apps/web/package.json | grep -E '"next"|"@next/bundle-analyzer"'

Repository: solid-connection/solid-connect-web

Length of output: 141


🌐 Web query:

@next/bundle-analyzer Next.js 14 compatibility requirements version

💡 Result:

For Next.js 14, use @next/bundle-analyzer v14.x (same major as Next). In practice, keep it in lockstep with your next version (e.g., next@14.2.30@next/bundle-analyzer@14.2.30). The npm registry shows published 14.x releases (e.g., 14.2.30) for @next/bundle-analyzer. [1]

Requirements to run it with Next 14:

  • Next.js 14 requires Node.js >= 18.17. [2]
  • It’s a Webpack-based analyzer; if you run next dev --turbo (Turbopack), you may see warnings because Webpack config/plugins (like bundle analyzer) aren’t supported the same way. [3]

Install example (pin to your Next 14 version):

npm i -D `@next/bundle-analyzer`@14.2.30

Sources: [1] [2] [3]


@next/bundle-analyzer 버전이 Next.js 메이저 버전과 맞지 않습니다.

현재 프로젝트에서 next^14.2.35이지만, @next/bundle-analyzer^16.1.6으로 설정되어 있어요. @next/* 패키지들은 Next.js의 메이저 버전과 동일하게 유지해야 호환성이 보장됩니다. 공식 문서에서도 Next.js 14에는 @next/bundle-analyzer v14.x를 사용하도록 권장하고 있습니다.

다음과 같이 수정을 제안드립니다:

  1. @next/bundle-analyzer 버전을 Next.js 14과 맞춰서 업그레이드하기
🔧 버전 수정 방안
-    "@next/bundle-analyzer": "^16.1.6",
+    "@next/bundle-analyzer": "^14.2.35",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@next/bundle-analyzer": "^16.1.6",
"@next/bundle-analyzer": "^14.2.35",
🤖 Prompt for AI Agents
In `@apps/web/package.json` at line 50, The dependency `@next/bundle-analyzer` in
apps/web package.json is pinned to ^16.1.6 which mismatches the project's
Next.js version (^14.2.35); update the "@next/bundle-analyzer" entry to a 14.x
compatible version (e.g. ^14.0.0 or the specific v14.x that matches Next 14) so
the `@next/`* major versions align, then reinstall deps (npm/yarn/pnpm install)
and run the app/build to verify compatibility; locate the
"@next/bundle-analyzer" entry in package.json to make this change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @.npmrc:
- Around line 3-5: The .npmrc contains conflicting hoisting settings: remove the
redundant public-hoist-pattern[]='*' entries (and the negations '!@types/react'
/ '!@types/react-dom' which are ineffective when shamefully-hoist=true) or else
remove shamefully-hoist=true so public-hoist-patterns take effect; then move
bundle-size optimizations into Next.js config by adjusting next.config.js (use
transpilePackages and optimizePackageImports) rather than relying on broad pnpm
hoisting.

Comment thread .npmrc Outdated
Comment on lines +3 to +5
public-hoist-pattern[]='*'
public-hoist-pattern[]='!@types/react'
public-hoist-pattern[]='!@types/react-dom'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

pnpm public-hoist-pattern shamefully-hoist interaction configuration

💡 Result:

shamefully-hoist and public-hoist-pattern both control public hoisting (putting selected packages into the root node_modules so “phantom” deps become resolvable by app/tooling).

Interaction / precedence

  • shamefully-hoist=true is effectively the same as public-hoist-pattern[]=* (public-hoist everything). It also overrides any public-hoist-pattern you set. [1][2]
  • shamefully-hoist=false means pnpm uses public-hoist-pattern normally (only patterns you specify are publicly hoisted). [2]

Recommended configuration approach

Prefer targeted public hoisting via public-hoist-pattern (only for broken tooling that expects flat/root deps), instead of shamefully-hoist=true. [1]

Example .npmrc

# Targeted public hoisting (recommended)
public-hoist-pattern[]=*plugin*
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*prettier*

pnpm also supports excluding matches with ! (so you can broadly hoist, then carve out exceptions). [1][3]

public-hoist-pattern[]=*types*
public-hoist-pattern[]=!@types/react

“Hoist everything” (legacy/last resort)

shamefully-hoist=true
# equivalent to: public-hoist-pattern[]=*

[1]

Rule of thumb: if you set shamefully-hoist=true, don’t bother setting public-hoist-pattern—it will be treated as * anyway. [1][2]

Sources: pnpm .npmrc settings docs (public-hoist-pattern + shamefully-hoist) [1], pnpm 5.2.0 release notes describing override behavior [2], pnpm .npmrc docs noting exclusion patterns with ! [3].


🌐 Web query:

pnpm monorepo hoisting best practices Next.js bundle optimization

💡 Result:

pnpm monorepo hoisting: best practices (so you don’t break Next.js or hide missing deps)

  1. Prefer pnpm’s default “isolated” linker (node-linker=isolated) to keep dependency boundaries correct and avoid “phantom dependency” bugs (packages importing deps they didn’t declare). Only switch to node-linker=hoisted if you must support tooling/filesystems that can’t handle symlinks. [1]
  2. Avoid blanket “public” hoisting (publicHoistPattern=['*'] / shamefullyHoist=true) unless you’re working around a specific broken tool, because it intentionally makes phantom deps available to app code. [2]
  3. If you need hoisting, make it targeted:
    • Use hoistPattern to hoist only the problematic toolchain deps (recommended by pnpm) instead of everything. [2]
    • Use publicHoistPattern sparingly for tools that incorrectly resolve plugins/deps and require root-level visibility. [2]

Typical .npmrc / pnpm config approach

# default / recommended
node-linker=isolated

# if some tool breaks, prefer targeted hoisting (examples)
# hoist-pattern[]=*eslint*
# hoist-pattern[]=*babel*
# public-hoist-pattern[]=*plugin*

(Exact config file can be .npmrc or workspace config depending on how you manage pnpm settings; the knobs are nodeLinker, hoistPattern, publicHoistPattern.) [1][2]


Next.js in a pnpm monorepo: bundle optimization checklist

  1. Bundle/transpile your internal workspace packages (the usual monorepo fix) using transpilePackages so Next can compile code coming from workspace packages / node_modules. [3][4]
// next.config.js
module.exports = {
  transpilePackages: ['@acme/ui', '@acme/shared'],
}
  1. Measure first with the official bundle analyzer so you can see what’s actually inflating the client/server bundles. [5]
ANALYZE=true pnpm build
  1. Fix “barrel export” bloat with experimental.optimizePackageImports (or rely on defaults for popular libs). This can reduce work during builds and avoid pulling in huge module graphs for big “named export” packages. [5][6][7]
module.exports = {
  experimental: {
    optimizePackageImports: ['some-big-barrel-lib'],
  },
}
  1. Keep server-only deps out of bundling when appropriate using serverExternalPackages (useful for native/binary deps or packages that shouldn’t be bundled). [5]

How this ties together (pnpm hoisting ↔ Next bundling)

  • Don’t use broad hoisting to “make Next find things.” If Next can’t compile/import a workspace package cleanly, the right fix is usually transpilePackages (and proper dependency declarations), not making everything visible via public hoisting. [3][4][2]
  • Use hoisting only as a compatibility escape hatch, and keep it pattern-based. [2][1]

Sources: pnpm settings for nodeLinker/hoisting knobs [1][2]; Next.js docs for bundle analyzer, transpilePackages, and optimizePackageImports [5][3][4][6]; Vercel guidance on optimizePackageImports and barrel-export impact [7].


공존할 수 없는 설정: 중복을 제거하고 번들 최적화 방식을 재검토하세요

현재 설정에 두 가지 문제가 있습니다:

  1. 설정 중복으로 인한 비효율

    • 2번 라인의 shamefully-hoist=true가 이미 모든 의존성을 공개 호이스팅합니다
    • 3~5번 라인의 public-hoist-pattern[]='*'는 위 설정과 정확히 동일한 동작을 하므로 불필요합니다
    • pnpm 공식 문서에 따르면, shamefully-hoist=true를 설정하면 다른 public-hoist-pattern 설정은 무시됩니다
  2. 의도한 효과를 내지 못할 가능성

    • 4~5번 라인의 React 타입 제외 설정(!@types/react 등)은 shamefully-hoist=true 앞에서 작동하지 않습니다
    • React 타입도 결국 공개 호이스팅되므로, 의존성 충돌 방지 효과가 없습니다
  3. 번들 최적화 목표 재검토

    • 광범위한 호이스팅만으로는 Next.js 번들 크기를 줄일 수 없습니다
    • 실제 최적화는 next.config.jstranspilePackagesoptimizePackageImports 설정으로 이루어집니다

권장 조치:

  • 불필요한 public-hoist-pattern[]='*'를 제거하거나
  • shamefully-hoist=true로 통일하되, 번들 최적화는 Next.js 설정으로 진행하세요
🤖 Prompt for AI Agents
In @.npmrc around lines 3 - 5, The .npmrc contains conflicting hoisting
settings: remove the redundant public-hoist-pattern[]='*' entries (and the
negations '!@types/react' / '!@types/react-dom' which are ineffective when
shamefully-hoist=true) or else remove shamefully-hoist=true so
public-hoist-patterns take effect; then move bundle-size optimizations into
Next.js config by adjusting next.config.js (use transpilePackages and
optimizePackageImports) rather than relying on broad pnpm hoisting.

@manNomi manNomi force-pushed the optimize/bundle-size branch from d7cac40 to 294670f Compare February 14, 2026 09:42
@manNomi manNomi merged commit a96d895 into main Feb 14, 2026
7 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant