กลยุทธ์การแยกกลุ่ม Webpack ที่ใหม่กว่าใน Next.js และ Gatsby จะลดโค้ดที่ซ้ำกันเพื่อปรับปรุงประสิทธิภาพการโหลดหน้าเว็บ
Chrome กำลังทำงานร่วมกันกับเครื่องมือและเฟรมเวิร์กในระบบนิเวศโอเพนซอร์สของ JavaScript เมื่อเร็วๆ นี้เราได้เพิ่มการเพิ่มประสิทธิภาพใหม่ๆ จำนวนหนึ่งเพื่อปรับปรุงประสิทธิภาพการโหลดของ Next.js และ Gatsby บทความนี้ครอบคลุมกลยุทธ์การแยกส่วนแบบละเอียดที่ได้รับการปรับปรุง ซึ่งตอนนี้จัดส่งโดยค่าเริ่มต้นในทั้ง 2 เฟรมเวิร์ก
บทนำ
Next.js และ Gatsby ใช้ webpack เป็น Bundler หลักเช่นเดียวกับเฟรมเวิร์กเว็บหลายๆ เฟรมเวิร์ก โดย webpack v3 ได้เปิดตัว CommonsChunkPlugin เพื่อให้สามารถแสดงผลโมดูลที่แชร์ระหว่างจุดแรกเข้าต่างๆ ในกลุ่ม "ทั่วไป" กลุ่มเดียว (หรือ 2-3 กลุ่ม) โค้ดที่ใช้ร่วมกันสามารถดาวน์โหลดแยกต่างหากและจัดเก็บไว้ในแคชของเบราว์เซอร์ตั้งแต่เนิ่นๆ ซึ่งจะส่งผลให้ประสิทธิภาพการโหลดดีขึ้น
รูปแบบนี้ได้รับความนิยมเนื่องจากเฟรมเวิร์กแอปพลิเคชันหน้าเว็บเดียวจำนวนมากใช้จุดแรกเข้าและ การกำหนดค่า Bundle ที่มีลักษณะดังนี้

แม้ว่าแนวคิดในการรวมโค้ดโมดูลที่ใช้ร่วมกันทั้งหมดไว้ในกลุ่มเดียวจะใช้งานได้จริง แต่ก็มีข้อจำกัด โมดูลที่ไม่ได้แชร์ในทุกจุดแรกเข้าสามารถดาวน์โหลดได้สำหรับเส้นทางที่ไม่ได้ใช้โมดูลดังกล่าว ซึ่งส่งผลให้มีการดาวน์โหลดโค้ดมากกว่าที่จำเป็น ตัวอย่างเช่น เมื่อ page1 โหลดกลุ่ม common ระบบจะโหลดโค้ดสำหรับ moduleC แม้ว่า page1 จะไม่ได้ใช้ moduleC ก็ตาม
ด้วยเหตุผลนี้และเหตุผลอื่นๆ อีก 2-3 ข้อ webpack v4 จึงนำปลั๊กอินออกเพื่อใช้ปลั๊กอินใหม่ SplitChunksPlugin
การแบ่งกลุ่มข้อมูลที่ปรับปรุงแล้ว
การตั้งค่าเริ่มต้นสำหรับ SplitChunksPlugin เหมาะสำหรับผู้ใช้ส่วนใหญ่ ระบบจะสร้างกลุ่มที่แยกกันหลายกลุ่มโดยขึ้นอยู่กับเงื่อนไขหลายประการเพื่อป้องกันการดึงข้อมูลโค้ดที่ซ้ำกันในหลายเส้นทาง
อย่างไรก็ตาม เฟรมเวิร์กเว็บจำนวนมากที่ใช้ปลั๊กอินนี้ยังคงใช้วิธี "คอมมอนส์เดียว" ในการแยกกลุ่ม ตัวอย่างเช่น Next.js จะสร้างแพ็กเกจ commons ที่มีโมดูลที่ใช้ในหน้าเว็บมากกว่า 50% และทรัพยากร Dependency ของเฟรมเวิร์กทั้งหมด (react, react-dom และอื่นๆ)
const splitChunksConfigs = {
…
prod: {
chunks: 'all',
cacheGroups: {
default: false,
vendors: false,
commons: {
name: 'commons',
chunks: 'all',
minChunks: totalPages > 2 ? totalPages * 0.5 : 2,
},
react: {
name: 'commons',
chunks: 'all',
test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-subscription)[\\/]/,
},
},
},
แม้ว่าการรวมโค้ดที่ขึ้นอยู่กับเฟรมเวิร์กไว้ในกลุ่มที่ใช้ร่วมกันจะทำให้สามารถดาวน์โหลดและแคชโค้ดดังกล่าวสำหรับจุดแรกเข้าใดก็ได้ แต่การใช้ฮิวริสติกตามการใช้งานในการรวมโมดูลทั่วไปที่ใช้ในหน้าเว็บมากกว่าครึ่งหนึ่งนั้นไม่มีประสิทธิภาพมากนัก การแก้ไขอัตราส่วนนี้จะส่งผลให้เกิดผลลัพธ์อย่างใดอย่างหนึ่งจาก 2 ข้อต่อไปนี้เท่านั้น
- หากลดอัตราส่วน ระบบจะดาวน์โหลดโค้ดที่ไม่จำเป็นมากขึ้น
- หากเพิ่มอัตราส่วน โค้ดจะซ้ำกันมากขึ้นในหลายเส้นทาง
เพื่อแก้ปัญหานี้ Next.js จึงใช้การกำหนดค่าที่แตกต่างกันสำหรับSplitChunksPlugin ซึ่งช่วยลดโค้ดที่ไม่จำเป็นสำหรับเส้นทางใดๆ
- โมดูลของบุคคลที่สามที่มีขนาดใหญ่เพียงพอ (มากกว่า 160 KB) จะแยกออกเป็นกลุ่มของตัวเอง
- ระบบจะสร้างกลุ่ม
frameworksแยกต่างหากสำหรับทรัพยากร Dependency ของเฟรมเวิร์ก (react,react-domและอื่นๆ) - ระบบจะสร้างกลุ่มที่ใช้ร่วมกันได้มากเท่าที่จำเป็น (สูงสุด 25 กลุ่ม)
- ขนาดขั้นต่ำสำหรับกลุ่มที่จะสร้างเปลี่ยนเป็น 20 KB
กลยุทธ์การแบ่งข้อมูลแบบละเอียดนี้มีประโยชน์ดังนี้
- เวลาในการโหลดหน้าเว็บดีขึ้น การปล่อยกลุ่มที่ใช้ร่วมกันหลายกลุ่มแทนที่จะเป็นกลุ่มเดียวจะช่วยลดจำนวนโค้ดที่ไม่จำเป็น (หรือซ้ำกัน) สำหรับจุดแรกเข้า
- การแคชที่ดีขึ้นระหว่างการไปยังส่วนต่างๆ การแยกไลบรารีขนาดใหญ่และการขึ้นต่อกันของเฟรมเวิร์กออกเป็นกลุ่มๆ จะช่วยลดโอกาสที่แคชจะใช้งานไม่ได้ เนื่องจากทั้ง 2 อย่างไม่น่าจะเปลี่ยนแปลงจนกว่าจะมีการอัปเกรด
คุณดูการกำหนดค่าทั้งหมดที่ Next.js นำมาใช้ได้ใน webpack-config.ts
คำขอ HTTP เพิ่มเติม
SplitChunksPlugin ได้กำหนดพื้นฐานสำหรับการแยกส่วนแบบละเอียด และการนำแนวทางนี้ไปใช้กับเฟรมเวิร์กอย่าง Next.js ไม่ใช่แนวคิดใหม่ทั้งหมด อย่างไรก็ตาม เฟรมเวิร์กจำนวนมากยังคงใช้กลยุทธ์การรวมกลุ่มแบบฮิวริสติกและ "ทั่วไป" เพียงอย่างเดียวด้วยเหตุผลบางประการ ซึ่งรวมถึงความกังวลว่าคำขอ HTTP จำนวนมากอาจส่งผลเสียต่อประสิทธิภาพของเว็บไซต์
เบราว์เซอร์สามารถเปิดการเชื่อมต่อ TCP ไปยังต้นทางเดียวได้ในจำนวนจำกัด (6 รายการสำหรับ Chrome) ดังนั้นการลดจำนวนกลุ่มที่ Bundler แสดงผลจะช่วยให้มั่นใจได้ว่าจำนวนคำขอทั้งหมดจะอยู่ภายใต้เกณฑ์นี้ อย่างไรก็ตาม วิธีนี้ใช้ได้กับ HTTP/1.1 เท่านั้น การมัลติเพล็กซ์ใน HTTP/2 ช่วยให้สามารถสตรีมคำขอหลายรายการพร้อมกันได้โดยใช้การเชื่อมต่อเดียวผ่านต้นทางเดียว กล่าวคือ โดยทั่วไปเราไม่จำเป็นต้องกังวลเกี่ยวกับการจำกัดจำนวนกลุ่มที่ Bundler สร้างขึ้น
เบราว์เซอร์หลักทั้งหมดรองรับ HTTP/2 ทีม Chrome และ Next.js ต้องการดูว่าการเพิ่มจำนวนคำขอโดยการแยกแพ็กเกจ "คอมมอนส์" รายการเดียวของ Next.js ออกเป็นกลุ่มที่แชร์หลายกลุ่มจะส่งผลต่อประสิทธิภาพการโหลดหรือไม่ โดยเริ่มจากการวัดประสิทธิภาพของเว็บไซต์เดียวขณะที่แก้ไขจำนวนคำขอแบบขนานสูงสุดโดยใช้พร็อพเพอร์ตี้ maxInitialRequests

ในการทดสอบหลายครั้งบนหน้าเว็บเดียวโดยเฉลี่ย 3 ครั้ง เวลา load, start-render และ First Contentful Paint ทั้งหมดจะยังคงเท่าเดิมเมื่อเปลี่ยนจำนวนคำขอเริ่มต้นสูงสุด (จาก 5 เป็น 15) สิ่งที่น่าสนใจคือเราสังเกตเห็นว่ามีค่าใช้จ่ายเพิ่มเติมด้านประสิทธิภาพเพียงเล็กน้อยหลังจากที่แยกคำขอออกเป็นหลายร้อยรายการ

ซึ่งแสดงให้เห็นว่าการคงจำนวนคำขอไว้ต่ำกว่าเกณฑ์ที่เชื่อถือได้ (20-25 คำขอ) จะช่วยสร้างสมดุลที่เหมาะสมระหว่างประสิทธิภาพการโหลดและประสิทธิภาพการแคช หลังจากทดสอบพื้นฐานแล้ว เราเลือก 25 เป็นจำนวน maxInitialRequest
การแก้ไขจำนวนคำขอสูงสุดที่เกิดขึ้นพร้อมกันส่งผลให้มี Bundle ที่ใช้ร่วมกันมากกว่า 1 รายการ และการแยก Bundle เหล่านั้นอย่างเหมาะสมสำหรับจุดแรกเข้าแต่ละจุดช่วยลดจำนวนโค้ดที่ไม่จำเป็นสำหรับหน้าเดียวกันได้อย่างมาก

การทดสอบนี้เป็นเพียงการแก้ไขจำนวนคำขอเพื่อดูว่าจะมีผลเสียต่อประสิทธิภาพการโหลดหน้าเว็บหรือไม่ ผลลัพธ์ชี้ให้เห็นว่าการตั้งค่า maxInitialRequests เป็น 25 ในหน้าทดสอบนั้นเหมาะสมที่สุด เนื่องจากช่วยลดขนาดเพย์โหลด JavaScript โดยไม่ทำให้หน้าเว็บช้าลง ปริมาณ JavaScript ทั้งหมดที่จำเป็นในการทำให้หน้าเว็บทำงานได้ยังคงเท่าเดิม ซึ่งอธิบายได้ว่าทำไมประสิทธิภาพการโหลดหน้าเว็บจึงไม่จำเป็นต้องดีขึ้นเมื่อลดปริมาณโค้ดลง
webpack ใช้ 30 KB เป็นขนาดขั้นต่ำเริ่มต้นสำหรับกลุ่มที่จะสร้าง อย่างไรก็ตาม การใช้ค่า maxInitialRequests เป็น 25 ร่วมกับขนาดขั้นต่ำ 20 KB กลับส่งผลให้การแคชดีขึ้น
การลดขนาดด้วยกลุ่มข้อมูลแบบละเอียด
เฟรมเวิร์กจำนวนมาก รวมถึง Next.js อาศัยการกำหนดเส้นทางฝั่งไคลเอ็นต์ (จัดการโดย JavaScript) เพื่อแทรกแท็กสคริปต์ใหม่สำหรับการเปลี่ยนเส้นทางทุกครั้ง แต่จะกำหนดข้อมูลแบบไดนามิกเหล่านี้ล่วงหน้าในเวลาบิลด์ได้อย่างไร
Next.js ใช้ไฟล์ Manifest ของบิลด์ฝั่งเซิร์ฟเวอร์เพื่อระบุว่าจุดแรกเข้าต่างๆ ใช้กลุ่มข้อมูลที่ส่งออกใด หากต้องการให้ข้อมูลนี้แก่ไคลเอ็นต์ด้วย ระบบจะสร้างไฟล์ Manifest ของบิลด์ฝั่งไคลเอ็นต์แบบย่อเพื่อแมปทรัพยากร Dependency ทั้งหมดสำหรับจุดแรกเข้าทุกจุด
// Returns a promise for the dependencies for a particular route
getDependencies (route) {
return this.promisedBuildManifest.then(
man => (man[route] && man[route].map(url => `/_next/${url}`)) || []
)
}
กลยุทธ์การแยกกลุ่มแบบละเอียดที่ใหม่กว่านี้เปิดตัวครั้งแรกใน Next.js โดยอยู่เบื้องหลัง Flag ซึ่งได้รับการทดสอบกับลูกค้ารายแรกสุดที่เปิดรับนวัตกรรมจำนวนหนึ่ง หลายรายพบว่า JavaScript ทั้งหมดที่ใช้ในเว็บไซต์ลดลงอย่างมาก ดังนี้
| เว็บไซต์ | การเปลี่ยนแปลง JS ทั้งหมด | % ความแตกต่าง |
|---|---|---|
| https://www.barnebys.com/ | -238 KB | -23% |
| https://sumup.com/ | -220 KB | -30% |
| https://www.hashicorp.com/ | -11 MB | -71% |
เวอร์ชันสุดท้ายจะจัดส่งโดยค่าเริ่มต้นในเวอร์ชัน 9.2
Gatsby
Gatsby เคยใช้วิธีการเดียวกันในการใช้การวิเคราะห์พฤติกรรมตามการใช้งานเพื่อกำหนดโมดูลทั่วไป ดังนี้
config.optimization = {
…
splitChunks: {
name: false,
chunks: `all`,
cacheGroups: {
default: false,
vendors: false,
commons: {
name: `commons`,
chunks: `all`,
// if a chunk is used more than half the components count,
// we can assume it's pretty global
minChunks: componentsCount > 2 ? componentsCount * 0.5 : 2,
},
react: {
name: `commons`,
chunks: `all`,
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
},
เมื่อเพิ่มประสิทธิภาพการกำหนดค่า webpack เพื่อใช้กลยุทธ์การแยกส่วนแบบละเอียดที่คล้ายกัน พวกเขายังพบว่า JavaScript ลดลงอย่างมากในเว็บไซต์ขนาดใหญ่หลายแห่งด้วย
| เว็บไซต์ | การเปลี่ยนแปลง JS ทั้งหมด | % ความแตกต่าง |
|---|---|---|
| https://www.gatsbyjs.org/ | -680 KB | -22% |
| https://www.thirdandgrove.com/ | -390 KB | -25% |
| https://ghost.org/ | -1.1 MB | -35% |
| https://reactjs.org/ | -80 KB | -8% |
ดู PR เพื่อทำความเข้าใจวิธีที่พวกเขานำตรรกะนี้ไปใช้ในการกำหนดค่า webpack ซึ่งจัดส่งโดยค่าเริ่มต้นใน v2.20.7
บทสรุป
แนวคิดของการจัดส่งข้อมูลแบบแยกส่วนไม่ได้มีไว้สำหรับ Next.js, Gatsby หรือแม้แต่ webpack โดยเฉพาะ ทุกคนควรพิจารณาปรับปรุงกลยุทธ์การแยกส่วนของแอปพลิเคชันหากเป็นไปตามแนวทาง Bundle "ทั่วไป" ขนาดใหญ่ ไม่ว่าจะใช้เฟรมเวิร์กหรือ Bundler โมดูลใดก็ตาม
- หากต้องการดูการเพิ่มประสิทธิภาพการแยกกลุ่มแบบเดียวกันที่ใช้กับแอปพลิเคชัน React แบบธรรมดา ให้ดูแอป React ตัวอย่างนี้ ซึ่งใช้กลยุทธ์การแยกกลุ่มแบบละเอียดเวอร์ชันที่เรียบง่าย และช่วยให้คุณเริ่มใช้ตรรกะประเภทเดียวกันกับเว็บไซต์ได้
- สำหรับ Rollup ระบบจะสร้างกลุ่มข้อมูลอย่างละเอียดโดยค่าเริ่มต้น ดู
manualChunksหากต้องการกำหนดค่าลักษณะการทำงานด้วยตนเอง