[PERF] 성능 리포트 측정 오류 및 LCP 이미지 최적화 수정#126
Conversation
- LCP로 감지된 로고 이미지에 priority를 적용해 지연 로딩 경고를 해결했습니다
- localePrefix: always 설정에 맞춰 URL에 /en 프리픽스를 추가해 리다이렉트로 인한 측정 실패(NO_FCP)를 해결했습니다 - 존재하지 않는 /settings/term 경로를 실제 라우트 /settings/policy로 수정했습니다
- Turbopack이 더 이상 생성하지 않는 webpack 전용 매니페스트(app-build-manifest.json) 대신 app-paths-manifest.json과 라우트별 page_client-reference-manifest.js를 파싱하도록 변경했습니다 - 모든 라우트가 공유 번들 크기로만 표시되던 문제를 해결했습니다
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 48 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
WalkthroughNext.js Turbopack 산출물 구조에 맞춰 Changes성능 리포트 및 LCP 최적화
Estimated code review effort: 2 (Simple) | ~12 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 로고 props를 줄줄이 나열한 정성이 눈에 띄네요 — 가독성 점수 +1점입니다! 📸 참고로 next/image의 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Timo Performance ReportBundle Size — timo-web
Lighthouse — timo-web
Image Optimization — timo-web
측정 커밋: |
- /en 루트 페이지가 빈 프래그먼트만 반환해 콘텐츠가 전혀 없어 NO_FCP로 항상 실패하던 URL을 측정 목록에서 제외했습니다
- pull_request 트리거에 synchronize를 추가해 새 커밋 push 시에도 워크플로우가 재실행되고 기존 코멘트가 업데이트되도록 했습니다
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/scripts/performance-report.js:
- Around line 35-44: The greedy regex in extractClientReferenceManifest can
capture too much when the manifest contains multiple assignments, causing
JSON.parse to fail and silently return null. Replace the regex-based extraction
with a more robust approach in extractClientReferenceManifest, such as locating
the target assignment using lastIndexOf and slicing only the intended JSON
object before parsing. Keep the null fallback for missing files or parse errors,
but ensure the captured substring is limited to the actual manifest payload.
- Around line 56-75: The bundle analysis in analyzeBundle currently assumes the
current Turbopack manifest shape for build-manifest.json and
app-paths-manifest.json, so add a lightweight CI schema check for those build
artifacts before performance-report.js relies on rootMainFiles, entryJSFiles,
and the app-paths entries. Update the performance-report pipeline around
analyzeBundle/extractClientReferenceManifest to validate the expected manifest
keys and fail fast or warn clearly when the structure changes, so silent
near-zero size calculations do not slip through.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: af036656-3ec4-4f9f-9cfa-9a0f252bc8b8
📒 Files selected for processing (3)
.github/scripts/performance-report.jsapps/timo-web/components/layout/sidebar/navigation/NavigationSidebar.tsxapps/timo-web/lighthouserc.cjs
| const extractClientReferenceManifest = (jsFilePath) => { | ||
| if (!jsFilePath || !fs.existsSync(jsFilePath)) return null; | ||
| try { | ||
| const content = fs.readFileSync(jsFilePath, 'utf8'); | ||
| const match = content.match(/"\]\s*=\s*(\{[\s\S]*\});\s*$/); | ||
| return match ? JSON.parse(match[1]) : null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| }; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
정규식 파싱의 안정성 개선을 권장합니다
extractClientReferenceManifest의 정규식 "\]\s*=\s*(\{[\s\S]*\});\s*$에서 [\s\S]*가 greedy하게 동작합니다. 매니페스트 파일에 여러 할당문이 존재할 경우, 첫 번째 {부터 마지막 }까지 모두 캡처하여 중간 할당문 내용이 포함되고 JSON.parse가 실패합니다. 실패 시 null이 반환되어 해당 라우트의 크기가 0바이트로 보고되는 침묵 오류가 발생합니다.
lastIndexOf를 활용한 방식이 더 견고합니다.
♻️ 제안하는 리팩터
const extractClientReferenceManifest = (jsFilePath) => {
if (!jsFilePath || !fs.existsSync(jsFilePath)) return null;
try {
const content = fs.readFileSync(jsFilePath, 'utf8');
- const match = content.match(/"\]\s*=\s*(\{[\s\S]*\});\s*$/);
- return match ? JSON.parse(match[1]) : null;
+ const lastAssign = content.lastIndexOf('"]');
+ if (lastAssign === -1) return null;
+ const jsonStart = content.indexOf('{', lastAssign);
+ const jsonEnd = content.lastIndexOf('}');
+ if (jsonStart === -1 || jsonEnd === -1 || jsonStart > jsonEnd) return null;
+ return JSON.parse(content.slice(jsonStart, jsonEnd + 1));
} catch {
return null;
}
};📝 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.
| const extractClientReferenceManifest = (jsFilePath) => { | |
| if (!jsFilePath || !fs.existsSync(jsFilePath)) return null; | |
| try { | |
| const content = fs.readFileSync(jsFilePath, 'utf8'); | |
| const match = content.match(/"\]\s*=\s*(\{[\s\S]*\});\s*$/); | |
| return match ? JSON.parse(match[1]) : null; | |
| } catch { | |
| return null; | |
| } | |
| }; | |
| const extractClientReferenceManifest = (jsFilePath) => { | |
| if (!jsFilePath || !fs.existsSync(jsFilePath)) return null; | |
| try { | |
| const content = fs.readFileSync(jsFilePath, 'utf8'); | |
| const lastAssign = content.lastIndexOf('"]'); | |
| if (lastAssign === -1) return null; | |
| const jsonStart = content.indexOf('{', lastAssign); | |
| const jsonEnd = content.lastIndexOf('}'); | |
| if (jsonStart === -1 || jsonEnd === -1 || jsonStart > jsonEnd) return null; | |
| return JSON.parse(content.slice(jsonStart, jsonEnd + 1)); | |
| } catch { | |
| return null; | |
| } | |
| }; |
🧰 Tools
🪛 ast-grep (0.44.1)
[warning] 37-37: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(jsFilePath, 'utf8')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/scripts/performance-report.js around lines 35 - 44, The greedy regex
in extractClientReferenceManifest can capture too much when the manifest
contains multiple assignments, causing JSON.parse to fail and silently return
null. Replace the regex-based extraction with a more robust approach in
extractClientReferenceManifest, such as locating the target assignment using
lastIndexOf and slicing only the intended JSON object before parsing. Keep the
null fallback for missing files or parse errors, but ensure the captured
substring is limited to the actual manifest payload.
- onboarding, settings, settings/account, settings/policy가 전부 빈 프래그먼트만 반환하는 placeholder 페이지임을 확인해 측정 목록에서 제외했습니다 - 실제 콘텐츠가 있는 home/today/focus/statistics만 남겼습니다
jjangminii
left a comment
There was a problem hiding this comment.
어느 순간부터 Lighthouse가 아예 동작하지 않고 있었는데 원인 찾아주셔서 감사해요 😊
ko locale 제외 판단도 적절한 것 같아요! JS 번들이 로케일과 무관하게 동일하니 굳이 두 번 측정할 필요는 없을 것 같아요. 간단한 코멘트 남겨뒀으니 확인 부탁드려요~
| const appPathsManifest = readJson(path.join(BUILD_DIR, 'server', 'app-paths-manifest.json')); | ||
| if (!buildManifest || !appPathsManifest) return null; | ||
|
|
||
| const sharedFiles = new Set(buildManifest.rootMainFiles ?? []); |
There was a problem hiding this comment.
PR 설명에도 적어주셨듯이 page_client-reference-manifest.js는 Next.js 비공식 내부 산출물이라, rootMainFiles나 entryJSFiles 키가 바뀌면 번들 크기가 조용히 0으로 계산돼요. CI에서 명시적으로 감지할 수 있도록 최소한의 검증을 추가하는 게 어떨까요?
const analyzeBundle = () => {
const buildManifest = readJson(...);
if (!Array.isArray(buildManifest?.rootMainFiles)) {
console.warn('[bundle] rootMainFiles 없음 ');
return null;
}
// ...
};
| "http://localhost:3000/en/home", | ||
| "http://localhost:3000/en/today", | ||
| "http://localhost:3000/en/focus", | ||
| "http://localhost:3000/en/statistics", |
There was a problem hiding this comment.
PR 설명에 존재하지 않던 /settings/term을 실제 라우트 /settings/policy로 수정했습니다" 라고 적어주셨는데 해당 페이지가 아직 머지되지않아 의도적으로 뺴두신게 맞을까요?
There was a problem hiding this comment.
옙`~~ 페이지 내부에 아무 콘텐츠가 없으면 CI가 실패하는 문제가 있어서, 퍼블리싱 시작되는 대로 추가해야 할 것 같아요.
ehye1
left a comment
There was a problem hiding this comment.
Lighthouse 실패 이유가 리다이렉트 때문이었군요!! 원인 파악해서 수정해주신 덕분에 이제 CI에서도 성능 리포트를 확인할 수 있겠다 ~~ 수고하셨습니다👀⭐
- rootMainFiles가 배열이 아니면 경고를 남기고 리포트 생성을 중단했습니다 - 라우트별 entryJSFiles를 찾지 못하면 경고를 남기도록 했습니다
ISSUE 🔗
close #125
What is this PR? 🔍
PR 성능 리포트 코멘트에서 Lighthouse가 항상 측정 실패로 표시되고 번들 크기가 모든 라우트에서 동일하게 나오던 문제를 수정했습니다. 함께 발견한 네비게이션 로고의 LCP 경고도 같이 처리했습니다.
배경
lighthouserc.cjs는 로케일 프리픽스 없는 URL(/home등)을 그대로 사용했고, 번들 분석 스크립트는 webpack 빌드 산출물(app-build-manifest.json,static/chunks/app/<route>/) 구조를 전제로 작성돼 있었습니다.next-intl이localePrefix: "always"로 모든 경로를/en,/ko로 리다이렉트하면서 Lighthouse가 리다이렉트 중NO_FCP로 9개 URL 전부 측정 실패했고, Next.js 16의 기본 번들러인 Turbopack은 webpack 전용 매니페스트를 생성하지 않아 번들 분석 스크립트가 모든 라우트를 공유 번들 크기로만 표시했습니다.네비게이션 로고 LCP
NavigationSidebar.tsx의 로고Image에priority를 추가했습니다.next/image의 기본 lazy loading으로 인해 로딩이 지연된다는 경고가 발생했습니다.priority를 적용해 eager loading +fetchpriority="high"+ preload 힌트가 자동 적용되도록 했습니다.Lighthouse 측정 URL
lighthouserc.cjs의collect.url목록을 전부/en프리픽스 포함 경로로 변경하고, 존재하지 않던/settings/term을 실제 라우트/settings/policy로 수정했습니다.requestedUrl: "http://localhost:3000/",finalDisplayedUrl: "http://localhost:3000/en"으로 리다이렉트가 발생하는 과정에서 Chrome이NO_FCP(콘텐츠를 전혀 페인트하지 못함) 에러를 내며 9개 URL 전부 실패하고 있었습니다.ko는 의도적으로 제외했는데, JS 번들이 로케일과 무관하게 동일해 성능 점수 자체는 사실상 같고, 추가하면numberOfRuns: 3과 곱해져 CI 시간이 2배로 늘어나기 때문입니다.번들 크기 분석 스크립트
.github/scripts/performance-report.js의analyzeBundle()을 Turbopack 산출물 구조에 맞게 재작성했습니다.pnpm turbo run build --filter=timo-web으로 실제 프로덕션 빌드를 돌려 확인한 결과, Turbopack은app-build-manifest.json도static/chunks/app/디렉터리도 생성하지 않아 기존 스크립트가 모든 라우트를pageBytes = 0,firstLoad = sharedBytes로만 계산하고 있었습니다.server/app-paths-manifest.json으로 라우트 → 파일 경로를 매핑하고, 각 라우트의page_client-reference-manifest.js(Turbopack이 생성하는 RSC 클라이언트 레퍼런스 매니페스트)에서entryJSFiles["[project]/apps/timo-web/app<라우트키>"]를 읽어 해당 페이지가 실제로 필요로 하는 청크 목록을 가져옵니다. 이 목록은 페이지 자신의 청크와 상위 레이아웃 청크가 이미 누적되어 있어, 공유 루트 청크(rootMainFiles)와 겹치는 파일만 제외하면 정확한 페이지별 First Load JS가 나옵니다. 로컬 빌드로 검증한 결과/[locale]/home은 53.81kB, 클라이언트 컴포넌트가 없는/[locale]/settings는 0B로 라우트마다 다르게 측정되는 것을 확인했습니다.page_client-reference-manifest.js는globalThis.__RSC_MANIFEST[...] = {...}형태의 JS 파일이라 JSON이 아니며, 내부 포맷은 Next.js가 공식 문서화하지 않은 빌드 산출물입니다. 정규식으로 마지막"] = {...};블록을 추출해 파싱하므로, 향후 Next.js 버전에서 이 산출물 구조가 바뀌면 다시 깨질 수 있습니다.To Reviewers
번들 분석 스크립트가 Next.js의 비공식 내부 빌드 산출물(
page_client-reference-manifest.js)에 의존하는 구조라, Next.js 버전을 올릴 때 이 부분이 깨지지 않는지 한번 확인해 주세요.Lighthouse는
en만 측정하고ko는 의도적으로 제외했는데, 이 판단이 맞는지도 봐주시면 좋겠습니다.Screenshot 📷
Test Checklist ✔
pnpm --filter timo-web check-types통과pnpm --filter timo-web lint통과pnpm turbo run build --filter=timo-web로 실제 프로덕션 빌드 후 번들 분석 로직을 로컬에서 재현·검증 (라우트별 크기가 서로 다르게 나옴을 확인)