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

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

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

บทนำ

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

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

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

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

การแบ่งกลุ่มที่ปรับปรุงแล้ว

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

อย่างไรก็ตาม เฟรมเวิร์กเว็บจำนวนมากที่ใช้ปลั๊กอินนี้ยังคงใช้แนวทาง "single-commons" ในการแยกโค้ด เช่น 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)[\\/]/,
      },
    },
  },

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

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

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

  • โมดูลของบุคคลที่สามที่มีขนาดใหญ่เพียงพอ (มากกว่า 160 KB) จะแยกออกเป็น ก้อนข้อมูลของตัวเอง
  • ระบบจะสร้างframeworks Chunk แยกต่างหากสำหรับทรัพยากร 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 ต้องการดูว่าการเพิ่มจำนวนคำขอโดยการแยกแพ็กเกจ "commons" รายการเดียวของ Next.js ออกเป็นหลายๆ ชิ้นที่แชร์กันจะส่งผลต่อประสิทธิภาพการโหลดในทางใดทางหนึ่งหรือไม่ โดยเริ่มจากการวัด ประสิทธิภาพของเว็บไซต์เดียวขณะปรับเปลี่ยนจำนวนคำขอแบบขนานสูงสุดโดยใช้พร็อพเพอร์ตี้ maxInitialRequests

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

จากการทดสอบหลายครั้งบนหน้าเว็บเดียวโดยเฉลี่ย 3 ครั้ง 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 การบิลด์ฝั่งเซิร์ฟเวอร์เพื่อพิจารณาว่า Chunk เอาต์พุตใดที่ใช้โดย จุดแรกเข้าต่างๆ เราจึงสร้างไฟล์ Manifest ของบิลด์ฝั่งไคลเอ็นต์แบบย่อเพื่อแมปการอ้างอิงทั้งหมดสำหรับทุกจุดแรกเข้า เพื่อให้ข้อมูลนี้แก่ไคลเอ็นต์ด้วย

// 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 "ทั่วไป" ขนาดใหญ่ ไม่ว่าจะเป็นเฟรมเวิร์กหรือเครื่องมือรวมโมดูลใดก็ตาม

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