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

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

การลดเพย์โหลด 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

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 ซึ่งจัดส่งโดยค่าเริ่มต้นใน v2.20.7

บทสรุป

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

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