Next(.js) on Page กับปัญหา Your Worker exceeded the size limit of XX MB

Problem Solving
Published 2024-12-20
POST

ไม่ว่าจะด้วยเหตุผลใดก็ตาม ที่อยากจะนำ Next.JS มา deploy ลง Cloudflare Page และอยากใช้ Server-side Rendering (SSR) ทำให้ลงเอยไปใช้ next-on-pages ที่รันบน Cloudflare Page Function (wrapper Cloudflare Worker)

หากสนใจเฉพาะทางออก คลิกเพื่อกระโดดไปตอนท้ายได้เลย

ปัญหา

เมื่อเราเพิ่มฟีเจอร์ให้เว็ปเราไปถึงจุดหนึ่ง ก็ไม่ใช่เรื่องแปลกที่จะเจอข้อจำกัด หรือ Error อย่างในวันนี้ หากเราตีความตาม Log ที่เห็น คือไฟล์ Function ที่เรา build ออกมา เวลารันคำสั่ง pnpm next-on-pages && wrangler pages deploy แล้วมีขนาดใหญ่เกิน 1 MiB (ปัจจุบัน Free Tier ให้ 3 MiB)

พอรู้ปัญหาแล้ว ทีนี้ เราจะแก้ยังดีต่อละ ??

กระบวนการไปหาทางออก

หาจุดเทียบเคียง

เวลาเราเจอปัญหาใหม่ๆ ก็เหมือนมืดแปดด้าน จะเริ่มยังไงดี

ส่วนตัว แนะนำให้หาจุดเทียบเคียงก่อน เทียบกับก่อนหน้าที่มีปัญหา

อย่างในกรณีนี้ เราก็ไปดูว่า ครั้งล่าสุดที่เรา build แล้วไม่พังคือตอนไหน

มองหาสิ่งที่เปลี่ยนไป

พอเรารู้แล้วว่าจุดเปลี่ยนจุดไหนที่เริ่มทำให้ build failed เราก็มาหาต้นตอกันต่อว่า อะไรคือที่มาของ error

อย่างแรกที่ควรลอง คือลอง checkout ไปทั้งสอง commit นั้น แล้ว build ออกมาดู เอา build output มาเทียบกัน แล้วเวลาเราทำงานกับ Next.JS เราก็นิยมดูผลลัพธ์จาก vercel build

bash | before.log
1
...
2
✓ Generating static pages (5/5)
3
Finalizing page optimization ...
4
Collecting build traces ...
5
6
Route (app) Size First Load JS
7
┌ ƒ / 36.8 kB 138 kB
8
...
9
├ ƒ /error 1.08 kB 102 kB
10
├ ○ /icon.svg 0 B 0 B
11
├ ○ /privacy-policy 1.07 kB 88.2 kB
12
├ ƒ /signin 1.08 kB 102 kB
13
├ ○ /terms-of-service 1.07 kB 88.2 kB
14
└ ƒ /workbench 7.93 kB 109 kB
15
+ First Load JS shared by all 87.2 kB
16
├ chunks/376-ae8867d1f8dbbcbb.js 31.5 kB
17
├ chunks/f14ca715-3ecd66d7a69888bb.js 53.6 kB
18
└ other shared chunks (total) 1.98 kB
19
20
21
ƒ Middleware 103 kB
22
(Static) prerendered as static content
23
ƒ (Dynamic) server-rendered on demand
24
...
bash | after.log
1
...
2
✓ Generating static pages (5/5)
3
Finalizing page optimization ...
4
Collecting build traces ...
5
6
Route (app) Size First Load JS
7
┌ ƒ / 36.8 kB 138 kB
8
...
9
├ ƒ /error 1.09 kB 102 kB
10
├ ○ /icon.svg 0 B 0 B
11
├ ƒ /payment-success 1.09 kB 102 kB
12
├ ○ /privacy-policy 1.07 kB 88.5 kB
13
├ ƒ /signin 1.09 kB 102 kB
14
├ ○ /terms-of-service 1.07 kB 88.5 kB
15
└ ƒ /workbench 95.4 kB 196 kB
16
+ First Load JS shared by all 87.4 kB
17
├ chunks/376-8534b4cf2341312a.js 31.7 kB
18
├ chunks/f14ca715-5320c06222168bec.js 53.6 kB
19
└ other shared chunks (total) 2.04 kB
20
21
22
ƒ Middleware 103 kB
23
(Static) prerendered as static content
24
ƒ (Dynamic) server-rendered on demand
25
...

จากการเทียบเคียง และสังเกต เราจะเห็นได้ว่า route /workbench มีขนาดใหญ่ขึ้น 7.93 kB -> 95.4 kB อย่างเห็นได้ชัด แต่ตัวเลขนี้ก็ยังดูห่างไกลจาก 1 MiB ไปอย่างมาก

หากเรามาลองคิดดูดีๆ อีกที จะพบว่า ตัวเลขที่ได้จาก vercel build เป็นขนาดที่ฝั่ง client จะได้รับ ไม่ใช่ขนาดของ Script ที่จะรันบน CF Page Function เพราะฉะนั้น เราควรมองหาขนาดที่แท้จริง ที่ Cloudflare ใช้ในการวัด

หลังจากลองไปอ่าน document และลองปะติดปะต่อ การทำงานของ next-on-page ได้พักหนึ่ง ก็ตกตะกอนว่า เราควรดูขนาดที่ได้จาก wrangler pages functions build --build-output-directory .vercel/output/static เพราะมันเป็นขนาดของ ไฟล์ที่ผ่าน build process ของ Cloudflare อีกที และจะถูกใช้อัพขึ้น CF Page Worker

bash | before.log
1
⚡️ Generated '.vercel/output/static/_worker.js/index.js'.
2
⚡️ Build completed in 1.73s
3
Attaching additional modules:
4
┌─────────────────────────────────────────────────────────────────────┬──────┬─────────────┐
5
Name │ Type │ Size │
6
├─────────────────────────────────────────────────────────────────────┼──────┼─────────────┤
7
...
8
__next-on-pages-dist__/functions/src/middleware.func.js │ esm │ 345.49 KiB │
9
├─────────────────────────────────────────────────────────────────────┼──────┼─────────────┤
10
__next-on-pages-dist__/functions/workbench.func.js │ esm │ 453.30 KiB │
11
├─────────────────────────────────────────────────────────────────────┼──────┼─────────────┤
12
...
13
__next-on-pages-dist__/webpack/f7f1f910724912e1fde53d8f6775020d.js │ esm │ 259.86 KiB │
14
├─────────────────────────────────────────────────────────────────────┼──────┼─────────────┤
15
Total (36 modules) │ │ 2363.35 KiB │
16
└─────────────────────────────────────────────────────────────────────┴──────┴─────────────┘
bash | after.log
1
⚡️ Generated '.vercel/output/static/_worker.js/index.js'.
2
⚡️ Build completed in 2.38s
3
Attaching additional modules:
4
┌─────────────────────────────────────────────────────────────────────┬──────┬─────────────┐
5
Name │ Type │ Size │
6
├─────────────────────────────────────────────────────────────────────┼──────┼─────────────┤
7
...
8
__next-on-pages-dist__/functions/src/middleware.func.js │ esm │ 345.49 KiB │
9
├─────────────────────────────────────────────────────────────────────┼──────┼─────────────┤
10
__next-on-pages-dist__/functions/workbench.func.js │ esm │ 2449.81 KiB │
11
├─────────────────────────────────────────────────────────────────────┼──────┼─────────────┤
12
...
13
__next-on-pages-dist__/webpack/f7f1f910724912e1fde53d8f6775020d.js │ esm │ 259.86 KiB │
14
├─────────────────────────────────────────────────────────────────────┼──────┼─────────────┤
15
Total (36 modules) │ │ 4450.53 KiB │
16
└─────────────────────────────────────────────────────────────────────┴──────┴─────────────┘

พอเห็นตัวเลขของไฟล์ ที่กระโดดมา 453.30 KiB -> 2449.81 KiB (~2.4 MiB) ก็จะเริ่มเห็นภาพชัดแล้วว่า ต้นตอของปัญหาจริงๆ อยู่ที่ไหน ซึ่งก็หนี้ไม่พ้น ไฟล์ __next-on-pages-dist__/functions/workbench.func.js ที่มีขนาดเกิน 1 MB

ข้อสังเกตุที่น่าสนใจคือ Worker Size Limit เขาวางกฏยังไง เขาดูที่ไฟล์ทั้งหมดรวมกัน หรือแยกกัน เพราะเวลาเราไปดูการ Routing หรือ Limit ก็ไม่มีตรงไหนบอกว่า เอาขนาดส่วนไหนมาคิด แต่พอเราดูตัวเลขจากข้างบน ไม่ว่าจะก่อน หรือหลังการแก้ไข 2363.35 KiB vs 4450.53 KiB ก็ล้วน เกิน 1 MB (Current 3 MB) ทั้งนั้น แสดงว่า เอาขนาดแต่ละไฟล์ย่อยมาคิด

พอเรารู้ว่าไฟล์ไหนมีขนาดเกิน เราก็ไล่ดูต่อว่า บรรทัดไหนที่เพิ่มเข้ามา โดยเริ่มที่จากการ ไล่ Diff (Difference) ระหว่าง 2 commit นี้ 1b6bf08 กับ cde7e3a

compare commit

นี้เป็นตัวอย่างที่ดีว่า ทำไมเราถึงควรเรียนรู้วิธีใช้ Git และเรียนรู้ว่า commit ที่ดีควรหน้าตายังไง ควรตั้งชื่อ commit ยังไง ใน commit ควรใหญ่แค่ไหน หรือเมื่อไหร่ควรแยก commit เมื่อไหร่ควร squash and merge หรือ merge commit เพราะถ้าเราเลือกถูกวิธี ผลลัพธ์ที่ควรได้คือ เรามี history ที่ชัดเจน สื่อสารเข้าใจว่ามีอะไรเกิดขึ้นกับโค๊ดบ้าง

เราก็จะกลับมาไล่ดูว่า ไฟล์ไหน ที่เกี่ยวข้องกับ src/app/workbench/page.tsx โดยดูว่ามี import หรือ import ของ import ไหนบ้างที่มีไฟล์ใน Diff 2 commit นี้

ในกรณีนี้คือ ไฟล์ AIBlock.tsx ที่เพิ่ม Markdown Parser แล้วตัว Parser นี้ก็มีขนาดใหญ่เกิน 1 MB เลยทำให้ workbench.func.js ขนาดเกิน

วิธีเช็คไวๆ ว่าฟีเจอร์ไหน ทำให้ขนาดเพิ่มมาเท่าไหร่ ให้ลอง Build เทียบระหว่างปิดฟีเจอร์ กับเวลาเปิดฟีเจอร์ ว่าขนาดเพิ่มขึ้นมาเท่าไหร่ หากนึกวิธีปิดฟีเจอร์ไม่ออก ก็ใช้วิธีการ Comment บรรทัดที่ใช้ฟีเจอร์นั้นๆ เอา

รับมือกับปัญหา

พอรู้แล้วว่าต้นตอของปัญหาคืออะไร เราก็ต้องเลือกวิธีจัดการกับปัญหานี้ ซึ่งก็ต้องลงมาดูว่าฟีเจอร์นี้ มีความสำคัญกับ SSR ไหม มีจังหวะไหน ที่เราต้องการ Render Markdown ก่อนถึงมือ Client ไหม

ในกรณีนี้ เราไม่ได้ใช้ Render Markdown ในฝั่ง SSR ดังนั้น เราสามารถเลือกที่จะ lazy-loaded React Component นี้ได้

ts | page.tsx
1
-
import { AIBlock } from "./AIBlock.tsx";
2
+
import { lazy } from "react";
3
+
const AIBlock = lazy(() =>
4
+
import("./AIBlock.tsx").then((m) => ({ default: m.AIBlock }))
5
+
);
6
// ...Function Render Component...

แก้เสร็จ เรามาลองเทสดู

ผลลัพธ์จาก vercel build มีขนาดเล็กลง แต่ wrangler pages functions build --build-output-directory .vercel/output/static กลับให้ขนาดที่แทบเท่าเดิม

bash | before.log
1
├───────────────────────────────────────────────────────┼──────┼─────────────┤
2
__next-on-pages-dist__/functions/workbench.func.js │ esm │ 2449.81 KiB │
3
├───────────────────────────────────────────────────────┼──────┼─────────────┤
bash | after.log
1
├───────────────────────────────────────────────────────┼──────┼─────────────┤
2
__next-on-pages-dist__/functions/workbench.func.js │ esm │ 2450.28 KiB │
3
├───────────────────────────────────────────────────────┼──────┼─────────────┤

Before vs After -> 2449.81 KiB vs 2450.28 KiB ไม่ค่อยเป็นที่น่าพึงพอใจเท่าไหร่ พอเราเห็นว่าผลลัพธ์ไม่ได้ดังที่คิด ก็เป็นเวลาที่ดีที่เราควรเปิดเช็ค Next.js Lazy Loading Documentations

หากเราอ่านเพียงเท่านี้ ก็อาจจะตีความได้ว่า React.lazy() ให้ผลลัพธ์เหมือนกันกับ next/dynamic แต่ถ้าเราเลื่อนลงไปดูตัวอย่าง Skipping SSR

จะพบว่า ถึงแม้ React.lazy() จะ lazy load จริงบน Client แต่บน Server ก็ยังทำการ pre-rendering ให้ด้วย เลยทำให้ workbench.func.js ต้องแนบฟีเจอร์ Render Markdown มาด้วย ส่งผลให้ขนาดไม่ได้ลดลง

ทางออก

ในที่สุดก็มาถึงทางออกจริงๆ แล้ว เพียงเราเปลี่ยนจาก React.lazy() ไปใช้ next/dynamic แทน รวมกับใส่ options { ssr: false }

ts | page.tsx
1
-
import { lazy } from "react";
2
-
const AIBlock = lazy(() =>
3
-
import("./AIBlock").then((m) => ({ default: m.AIBlock }))
4
-
);
5
+
import dynamic from "next/dynamic"
6
+
const AIBlock = dynamic(() =>
7
+
import("./AIBlock").then((m) => ({ default: m.AIBlock })),
8
+
{ ssr: false }
9
+
);
10
// ...Function Render Component...

แล้วเมื่อ build ออกมา

bash | before.log
1
├───────────────────────────────────────────────────────┼──────┼─────────────┤
2
__next-on-pages-dist__/functions/workbench.func.js │ esm │ 2449.81 KiB │
3
├───────────────────────────────────────────────────────┼──────┼─────────────┤
bash | after.log
1
├───────────────────────────────────────────────────────┼──────┼─────────────┤
2
__next-on-pages-dist__/functions/workbench.func.js │ esm │ 455.59 KiB │
3
├───────────────────────────────────────────────────────┼──────┼─────────────┤

ขนาดเหลือไม่ 1 MB แล้ว Deploy Success! Yay!

Happy Very Funny GIF by Disney Zootopia

ข้อควรระวัง เวลาเราใช้ React.lazy() หรือ next/dynamic อย่าลืมคิดถึงจังหวะที่ Component ต้อง lazy-load ด้วย เพราะเมื่อเราใช้วิธีเหล่านี้ จะทำให้เห็นจังหวะที่ UI กระตุก ซึ่งอาจทำให้ดูน่าหงุดหงิดสำหรับผู้ใช้

ทางออกที่มีคือ อาจเลือกที่จะใส่ Suspense หรือ options loading เพื่อแสดง fallback ระหว่างรอ Component กำลังโหลด

สรุป

เมื่อเราปัญหาที่ ต่อให้เสริช Stackoverflow แล้วไม่เจอ ก็ใช่ว่าจะไม่มีทางออกซะทีเดียว หากเพียงค่อยๆ มองหาทางออกอย่างเป็นระบบ เริ่มจากตีกรอบให้แคบลงมาจนหาต้นตอของปัญหาได้ แล้วมองหาวิธีแก้ ถ้าเสริชตรงๆไม่มี เราก็ต้องศึกษาเครื่องมือที่เราใช้เอาเอง เริ่มจากอ่าน docs เป็นสิ่งที่แนะนำอันดับแรก แต่หากไม่เพียงพอ เราก็ลองเช็ค Source Code ของเครื่องมือที่ใช้หากเป็น Open Source ถ้าไม่ใช่ ก็คงหนีไม่พ้น ต้องลอง Reverse engineer ไม่ก็เปลี่ยนไปใช้เครื่องมืออื่นแทน สุดท้ายเราจะลงเอยกับทางออกที่ลงตัวเอง

ไว้มีปัญหาอะไรแปลกๆ น่าสนใจ จะมาแชร์กันอีก เจอกันใหม่โพสหน้า~