ปรับปรุงประสิทธิภาพการโหลดหน้าเว็บของ Next.js และ Gatsby ด้วยการแบ่งข้อมูลเป็นกลุ่มแบบละเอียด

กลยุทธ์การแบ่งส่วนเว็บแพ็กใหม่ใน Next.js และ Gatsby ช่วยลดโค้ดที่ซ้ำกันเพื่อปรับปรุงประสิทธิภาพการโหลดหน้าเว็บ

ฮูเซน จอร์เดห์
ฮูสเซน จอร์เดห์

Chrome กำลังทำงานร่วมกันกับเครื่องมือและเฟรมเวิร์กในระบบนิเวศโอเพนซอร์สของ JavaScript เมื่อเร็วๆ นี้มีการเพิ่มการเพิ่มประสิทธิภาพใหม่ๆ จำนวนหนึ่งเพื่อปรับปรุงประสิทธิภาพการโหลดของ Next.js และ Gatsby บทความนี้กล่าวถึงกลยุทธ์การแบ่งส่วนแบบละเอียดที่ได้รับการปรับปรุง ซึ่งในขณะนี้จัดส่งไปโดยค่าเริ่มต้นในทั้ง 2 เฟรมเวิร์ก

เกริ่นนำ

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

รูปแบบนี้ได้รับความนิยมในเฟรมเวิร์กของแอปพลิเคชันหน้าเว็บเดียวจำนวนมากที่ใช้การกำหนดค่าจุดแรกเข้าและการรวมแพ็กเกจที่มีลักษณะดังนี้

จุดแรกเข้าทั่วไปและการกำหนดค่า Bundle

ถึงแม้แนวคิดในการรวมโค้ดโมดูลที่แชร์ทั้งหมดไว้ในกลุ่มเดียวก็มีข้อจำกัด โมดูลที่ไม่ได้ใช้ร่วมกันในจุดแรกเข้าทุกจุดสามารถดาวน์โหลดได้สำหรับเส้นทางที่ไม่ได้ใช้ ซึ่งส่งผลให้มีการดาวน์โหลดโค้ดมากเกินจำเป็น ตัวอย่างเช่น เมื่อ page1 โหลดกลุ่ม common ระบบจะโหลดโค้ดสำหรับ moduleC แม้ว่า page1 จะไม่ได้ใช้ moduleC ก็ตาม ด้วยเหตุนี้ Webpack v4 พร้อมกับคนอื่นๆ อีกจำนวนหนึ่งจึงนำปลั๊กอินออกเพื่อเปลี่ยนไปใช้ปลั๊กอินใหม่ SplitChunksPlugin

ผ่านการปรับปรุงเนื้อหา

การตั้งค่าเริ่มต้นของ SplitChunksPlugin เหมาะสำหรับผู้ใช้ส่วนใหญ่ ระบบจะสร้างส่วนแยกหลายรายการตามconditionsจำนวนหนึ่งเพื่อป้องกันไม่ให้มีการดึงข้อมูลโค้ดที่ซ้ำกันในหลายเส้นทาง

อย่างไรก็ตาม เว็บเฟรมเวิร์กจำนวนมากที่ใช้ปลั๊กอินนี้ยังคงเป็นไปตามแนวทาง "กลุ่มร่วมเดียว" ในการแยกส่วน เช่น Next.js จะสร้างแพ็กเกจ commons ที่มีโมดูลที่ใช้ในหน้าเว็บมากกว่า 50% และการอ้างอิงเฟรมเวิร์กทั้งหมด (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)[\\/]/,
      },
    },
  },

แม้ว่าการรวมโค้ดที่ขึ้นอยู่กับเฟรมเวิร์กไว้ในกลุ่มที่แชร์จะทำให้มีการดาวน์โหลดและแคชสำหรับจุดแรกเข้าได้ แต่การเรียนรู้ตามการใช้งานโดยการรวมโมดูลทั่วไปที่ใช้มากกว่าครึ่งหน้านั้นไม่มีประสิทธิภาพมากนัก การแก้ไขอัตราส่วนนี้จะทำให้เกิดผลลัพธ์แบบใดแบบหนึ่งเท่านั้น ดังนี้

  • หากคุณลดอัตราส่วน ระบบจะดาวน์โหลดโค้ดที่ไม่จำเป็นมากขึ้น
  • หากคุณเพิ่มอัตราส่วน ระบบจะทำซ้ำโค้ดจำนวนมากขึ้นในหลายๆ เส้นทาง

ในการแก้ปัญหานี้ Next.js จึงได้ใช้การกำหนดค่าที่ต่างกันสำหรับ SplitChunksPlugin ซึ่งช่วยลดโค้ดที่ไม่จำเป็นสำหรับทุกเส้นทาง

  • โมดูลของบุคคลที่สามที่มีขนาดใหญ่เพียงพอ (ใหญ่กว่า 160 KB) จะแบ่งออกเป็นส่วนๆ ของตัวเอง
  • ระบบจะสร้างกลุ่ม frameworks แยกต่างหากสำหรับทรัพยากร Dependency ของเฟรมเวิร์ก (react, react-dom และอื่นๆ)
  • สร้างกลุ่มที่แชร์ได้มากเท่าที่ต้องการ (สูงสุด 25 กลุ่ม)
  • ขนาดขั้นต่ำของส่วนที่จะสร้างขึ้นเปลี่ยนเป็น 20 KB

กลยุทธ์การแบ่งเนื้อหาแบบละเอียดนี้ให้ประโยชน์ดังต่อไปนี้

  • ปรับปรุงเวลาในการโหลดหน้าเว็บ การปล่อยกลุ่มที่แชร์หลายๆ ส่วน แทนที่จะเป็นรายการเดียว ลดจำนวนโค้ดที่ไม่จำเป็น (หรือซ้ำกัน) สำหรับจุดเข้าถึงใดๆ
  • ปรับปรุงการแคชในระหว่างการนำทาง การแยกไลบรารีขนาดใหญ่และทรัพยากร Dependency ของเฟรมเวิร์กออกเป็นกลุ่มๆ จะลดโอกาสที่แคชจะเป็นโมฆะ เนื่องจากทั้งคู่ไม่น่าจะเปลี่ยนแปลงจนกว่าจะทำการอัปเกรด

ดูการกำหนดค่าทั้งหมดที่ Next.js นำมาใช้ใน webpack-config.ts

คำขอ HTTP เพิ่มเติม

SplitChunksPlugin ได้กำหนดพื้นฐานสำหรับการแบ่งส่วนแบบละเอียด และการใช้แนวทางนี้กับเฟรมเวิร์กอย่าง Next.js ไม่ใช่แนวคิดใหม่ทั้งหมด อย่างไรก็ตาม เฟรมเวิร์กจำนวนมากยังคง ใช้กลยุทธ์แบบกลุ่มแบบฮิวริสติกและ "ทั่วไป" เพียง 2-3 เหตุผล ซึ่งรวมถึงความกังวลที่ว่าคำขอ HTTP จำนวนมากอาจส่งผลเสียต่อประสิทธิภาพของเว็บไซต์

เบราว์เซอร์จะเปิดการเชื่อมต่อ TCP ไปยังต้นทางเดียวในจำนวนที่จำกัดเท่านั้น (6 สำหรับ Chrome) ดังนั้นการลดจำนวนของกลุ่มที่แสดงผลโดย Bundler จะช่วยให้มั่นใจได้ว่าจำนวนคำขอทั้งหมดจะอยู่ภายใต้เกณฑ์นี้ อย่างไรก็ตาม ค่านี้จะเป็นจริงสำหรับ HTTP/1.1 เท่านั้น การทำ Multiplex ใน HTTP/2 ช่วยให้สตรีมคำขอหลายรายการพร้อมกันได้โดยใช้การเชื่อมต่อเดียวผ่านต้นทางเดียว กล่าวคือ โดยทั่วไปเราไม่ต้องกังวลเกี่ยวกับการจำกัดจำนวนส่วน ที่ Bundler ของเราปล่อยออกมา

เบราว์เซอร์หลักทั้งหมดรองรับ HTTP/2 ทีม Chrome และ Next.js ต้องการทราบว่าการเพิ่มจำนวนคำขอโดยแยกแพ็กเกจ "commons" เดี่ยวของ Next.js ออกเป็นหลายๆ ส่วนที่แชร์จะส่งผลต่อประสิทธิภาพการโหลดหรือไม่ โดยเริ่มต้นด้วยการวัดประสิทธิภาพของเว็บไซต์เดียวในขณะที่แก้ไขจำนวนคำขอพร้อมกันสูงสุดโดยใช้พร็อพเพอร์ตี้ maxInitialRequests

ประสิทธิภาพการโหลดหน้าเว็บที่มีจำนวนคำขอเพิ่มขึ้น

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

ประสิทธิภาพการโหลดหน้าเว็บที่มีคำขอหลายร้อยรายการ

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

การแก้ไขจำนวนคำขอสูงสุดที่เกิดขึ้นพร้อมกันจะทำให้มีแพ็กเกจที่แชร์มากกว่า 1 กลุ่ม และการแยกคำขออย่างเหมาะสมสำหรับจุดแรกเข้าแต่ละจุดลดจำนวนโค้ดที่ไม่จำเป็นสำหรับหน้าเดียวกันลงได้อย่างมาก

การลดเพย์โหลด JavaScript ด้วยการแบ่งส่วนข้อมูลเพิ่มขึ้น

การทดสอบนี้เป็นเพียงการแก้ไขจำนวนคำขอเพื่อดูว่าจะส่งผลในทางลบต่อประสิทธิภาพการโหลดหน้าเว็บไหม ผลลัพธ์ที่ได้ชี้ให้เห็นว่าการตั้งค่า 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

กลยุทธ์การแบ่งส่วนแบบละเอียดใหม่นี้เปิดตัวครั้งแรกใน 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%
การลดขนาด JavaScript - ในทุกเส้นทาง (บีบอัด)

เราจัดส่งเวอร์ชันสุดท้ายในเวอร์ชัน 9.2 โดยค่าเริ่มต้น

แกตส์บี้

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%
การลดขนาด JavaScript - ในทุกเส้นทาง (บีบอัด)

ดู PR เพื่อทำความเข้าใจวิธีที่ทีมนำตรรกะนี้ไปใช้กับการกำหนดค่า Webpack ซึ่งมีการจัดส่งตามค่าเริ่มต้นในเวอร์ชัน 2.20.7

บทสรุป

แนวคิดในการจัดส่งเป็นกลุ่มแบบละเอียดไม่ได้เจาะจงไปที่ Next.js, Gatsby หรือแม้แต่ Webpack ทุกคนควรพิจารณาปรับปรุงกลยุทธ์การแบ่งส่วนแอปพลิเคชันของตนเองหากเป็นไปตามแนวทางแบบกลุ่ม "Commons" ไม่ว่าจะใช้เฟรมเวิร์กหรือ Bundle Bundler แบบใดก็ตาม

  • หากคุณต้องการดูการเพิ่มประสิทธิภาพแบบแบ่งส่วนเดียวกับที่ใช้กับแอปพลิเคชัน Vanilla React ลองดูแอป React ตัวอย่างนี้ ซึ่งจะใช้กลยุทธ์การแบ่งส่วนแบบละเอียดเวอร์ชันที่เรียบง่าย และช่วยให้คุณเริ่มใช้ตรรกะประเภทเดียวกันในเว็บไซต์
  • สําหรับรายงาน ระบบจะสร้างกลุ่มแบบละเอียดโดยค่าเริ่มต้น ลองดูที่ manualChunks หากต้องการกำหนดค่าลักษณะการทำงานด้วยตนเอง