Code Velocity
เครื่องมือสำหรับนักพัฒนา

ประสิทธิภาพของบรรทัด Diff: การปีนเขาอันยากลำบากของ GitHub เพื่อการเพิ่มประสิทธิภาพ

·7 นาทีอ่าน·GitHub·แหล่งที่มา
แชร์
แผนภาพแสดงการปรับปรุงประสิทธิภาพในบรรทัด diff ของ GitHub เน้นการลดจำนวนโหนด DOM และฮีป JavaScript ในมุมมองที่ได้รับการปรับปรุง

การปีนเขาอันยากลำบากของ GitHub: การเพิ่มประสิทธิภาพบรรทัด Diff เพื่อประสิทธิภาพสูงสุด

พูลรีเควสต์เป็นหัวใจสำคัญของ GitHub ที่ซึ่งวิศวกรจำนวนนับไม่ถ้วนทุ่มเทเวลาส่วนสำคัญในชีวิตการทำงานของพวกเขา ด้วยขนาดที่มหาศาลของ GitHub การจัดการพูลรีเควสต์ที่มีตั้งแต่การแก้ไขเล็กน้อยเพียงบรรทัดเดียวไปจนถึงการเปลี่ยนแปลงขนาดใหญ่ที่ครอบคลุมไฟล์นับพันและโค้ดนับล้านบรรทัด ประสบการณ์การตรวจสอบจึงต้องรวดเร็วและตอบสนองเป็นพิเศษ การเปิดตัวประสบการณ์ใหม่บนพื้นฐาน React สำหรับแท็บ Files changed ซึ่งตอนนี้เป็นค่าเริ่มต้นสำหรับผู้ใช้ทุกคน ถือเป็นการลงทุนที่สำคัญในการรับรองประสิทธิภาพที่แข็งแกร่ง โดยเฉพาะอย่างยิ่งสำหรับพูลรีเควสต์ขนาดใหญ่ที่ท้าทายเหล่านี้ ความมุ่งมั่นนี้เกี่ยวข้องกับการแก้ปัญหาที่ยากลำบากอย่างต่อเนื่อง เช่น การเรนเดอร์ที่ได้รับการปรับปรุง เวลาแฝงในการโต้ตอบ และการใช้หน่วยความจำ

ก่อนการเพิ่มประสิทธิภาพเหล่านี้ ในขณะที่ผู้ใช้ส่วนใหญ่ได้รับประสบการณ์ที่ตอบสนอง แต่พูลรีเควสต์ขนาดใหญ่กลับนำไปสู่ประสิทธิภาพที่ลดลงอย่างเห็นได้ชัดอย่างหลีกเลี่ยงไม่ได้ ในกรณีที่รุนแรง ฮีป JavaScript เกิน 1 GB, จำนวนโหนด DOM เกิน 400,000 และการโต้ตอบกับหน้าเว็บกลายเป็นเรื่องช้ามากหรือไม่สามารถใช้งานได้เลย เมตริกการตอบสนองที่สำคัญ เช่น Interaction to Next Paint (INP) พุ่งสูงเกินระดับที่ยอมรับได้ ทำให้ผู้ใช้รู้สึกถึงความล่าช้าในการป้อนข้อมูลอย่างชัดเจน บทความนี้เจาะลึกการเดินทางอย่างละเอียดที่ GitHub ได้ดำเนินการเพื่อปรับปรุงเมตริกประสิทธิภาพหลักเหล่านี้อย่างมาก พลิกโฉมประสบการณ์การตรวจสอบ diff

การนำทางคอขวดด้านประสิทธิภาพ: แนวทางหลายกลยุทธ์

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

แทนที่จะเป็นเช่นนั้น ทีมวิศวกรของ GitHub ได้พัฒนาชุดกลยุทธ์ที่ครอบคลุม โดยแต่ละกลยุทธ์ได้รับการออกแบบมาอย่างพิถีพิถันเพื่อจัดการกับขนาดและความซับซ้อนของพูลรีเควสต์ที่แตกต่างกัน กลยุทธ์เหล่านี้สร้างขึ้นบนสามหัวข้อหลัก:

  1. การเพิ่มประสิทธิภาพที่เน้นไปที่คอมโพเนนต์บรรทัด Diff: การเพิ่มประสิทธิภาพของประสบการณ์ diff หลักสำหรับพูลรีเควสต์ส่วนใหญ่ สิ่งนี้ทำให้มั่นใจว่าการตรวจสอบขนาดกลางและขนาดใหญ่ยังคงรวดเร็วโดยไม่ลดทอนฟังก์ชันการทำงานที่คาดหวัง เช่น การค้นหาในหน้าแบบเนทีฟ
  2. การลดทอนประสิทธิภาพอย่างสวยงามด้วย Virtualization: การรับรองการใช้งานสำหรับพูลรีเควสต์ที่ใหญ่ที่สุดโดยการจัดลำดับความสำคัญของการตอบสนองและความเสถียร และการจำกัดสิ่งที่เรนเดอร์อย่างชาญฉลาดในแต่ละช่วงเวลา
  3. การลงทุนในส่วนประกอบพื้นฐานและการปรับปรุงการเรนเดอร์: การนำการปรับปรุงมาใช้เพื่อให้เกิดประโยชน์ที่เพิ่มขึ้นในทุกขนาดของพูลรีเควสต์ โดยไม่ขึ้นอยู่กับโหมดการดูเฉพาะของผู้ใช้

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

การวิเคราะห์ V1: ต้นทุนของบรรทัด Diff ที่มีราคาแพง

การใช้งาน React-based เริ่มต้นของ GitHub ซึ่งเรียกว่า v1 เป็นรากฐานสำหรับมุมมอง diff ที่ทันสมัย เวอร์ชันนี้เป็นความพยายามอย่างจริงจังในการย้ายมุมมอง Rails แบบคลาสสิกมายัง React โดยให้ความสำคัญกับการสร้างคอมโพเนนต์ React ขนาดเล็กที่นำกลับมาใช้ใหม่ได้ และการรักษาโครงสร้าง DOM tree ที่ชัดเจน อย่างไรก็ตาม แนวทางนี้ แม้จะดูมีเหตุผลในช่วงแรก แต่กลับกลายเป็นคอขวดที่สำคัญในระดับขนาดใหญ่

ใน v1 การเรนเดอร์แต่ละบรรทัด diff เป็นการดำเนินการที่ใช้ทรัพยากรมาก บรรทัดเดียวในมุมมองแบบรวมมักจะแปลงเป็น DOM element ประมาณ 10 รายการ ในขณะที่มุมมองแบบแยกต้องใช้ใกล้เคียง 15 รายการ จำนวนนี้จะเพิ่มขึ้นอีกด้วยการไฮไลท์ไวยากรณ์ ซึ่งจะเพิ่มแท็ก <span> เข้ามาอีกมาก ในชั้น React, diff แบบรวมมีคอมโพเนนต์อย่างน้อยแปดรายการต่อบรรทัด และมุมมองแบบแยกมีอย่างน้อย 13 รายการ เหล่านี้เป็นจำนวนพื้นฐาน โดยสถานะ UI เพิ่มเติม เช่น ความคิดเห็น การวางเมาส์ และการโฟกัส จะเพิ่มคอมโพเนนต์เข้าไปอีก

สถาปัตยกรรม v1 ยังประสบปัญหาการเพิ่มจำนวนของตัวจัดการเหตุการณ์ React แม้จะดูไม่เป็นอันตรายในขนาดเล็ก แต่บรรทัด diff เดียวสามารถมีตัวจัดการเหตุการณ์ 20 รายการหรือมากกว่านั้น เมื่อคูณด้วยหลายพันบรรทัดในพูลรีเควสต์ขนาดใหญ่ สิ่งนี้จะเพิ่มขึ้นอย่างรวดเร็ว ทำให้เกิดโอเวอร์เฮดที่มากเกินไปและการใช้ฮีป JavaScript ที่เพิ่มขึ้น ความซับซ้อนนี้ไม่เพียงส่งผลกระทบต่อประสิทธิภาพเท่านั้น แต่ยังทำให้การพัฒนาและการบำรุงรักษายากขึ้นอีกด้วย การออกแบบเริ่มต้น ซึ่งมีประสิทธิภาพสำหรับข้อมูลที่มีขอบเขต ประสบปัญหาอย่างมากเมื่อเผชิญกับลักษณะที่ไม่มีขอบเขตของขนาดพูลรีเควสต์ที่หลากหลายของ GitHub

สรุปได้ว่า สำหรับทุกบรรทัด diff ใน v1 ระบบมี:

  • องค์ประกอบ DOM tree อย่างน้อย 10-15 รายการ
  • คอมโพเนนต์ React อย่างน้อย 8-13 รายการ
  • ตัวจัดการเหตุการณ์ React อย่างน้อย 20 รายการ
  • คอมโพเนนต์ React ขนาดเล็กที่นำกลับมาใช้ใหม่ได้จำนวนมาก

สถาปัตยกรรมนี้สัมพันธ์โดยตรงกับขนาดพูลรีเควสต์ที่ใหญ่ขึ้นด้วย INP ที่ช้าลงและการใช้ฮีป JavaScript ที่เพิ่มขึ้น ทำให้จำเป็นต้องมีการประเมินและออกแบบใหม่ขั้นพื้นฐาน

การปฏิวัติการเรนเดอร์: ผลกระทบของการเพิ่มประสิทธิภาพ V2

การเปลี่ยนผ่านไปสู่ v2 ถือเป็นการยกเครื่องสถาปัตยกรรมครั้งสำคัญ โดยมุ่งเน้นไปที่การเปลี่ยนแปลงที่ละเอียดและมีผลกระทบ ทีมงานยอมรับปรัชญาที่ว่า 'ไม่มีการเปลี่ยนแปลงใดเล็กเกินไปเมื่อพูดถึงประสิทธิภาพ โดยเฉพาะอย่างยิ่งในระดับที่ใหญ่' ตัวอย่างสำคัญคือการลบแท็ก <code> ที่ไม่จำเป็นออกจากเซลล์หมายเลขบรรทัด แม้การลดจำนวนโหนด DOM สองรายการต่อบรรทัด diff อาจดูเล็กน้อย แต่สำหรับ 10,000 บรรทัด สิ่งนี้เทียบเท่ากับการลดโหนดใน DOM ลง 20,000 รายการทันที ซึ่งแสดงให้เห็นว่าการเพิ่มประสิทธิภาพแบบกำหนดเป้าหมายและค่อยเป็นค่อยไปสามารถนำมาซึ่งการปรับปรุงที่สำคัญได้อย่างไร

การเปรียบเทียบภาพด้านล่างเน้นความซับซ้อนที่ลดลงจาก v1 ไป v2 ในระดับคอมโพเนนต์:

V1 Diff Components and HTML. We had 8 react components for a single diff line. V2 Diff Components and HTML. We had 3 react components for a single diff line.

สถาปัตยกรรมการจัดองค์ประกอบที่คล่องตัว

นวัตกรรมหลักใน v2 คือการทำให้โครงสร้างคอมโพเนนต์ง่ายขึ้น ทีมงานลดจำนวนคอมโพเนนต์ React ต่อบรรทัด diff จากแปดเหลือสองรายการ สิ่งนี้ทำได้โดยการกำจัดโครงสร้างคอมโพเนนต์ที่ซ้อนกันลึก และสร้างคอมโพเนนต์เฉพาะสำหรับแต่ละบรรทัด diff แบบแยกและแบบรวม แม้ว่าจะมีการทำซ้ำโค้ดบางส่วน แต่มันช่วยลดความซับซ้อนในการเข้าถึงข้อมูลและลดความซับซ้อนโดยรวมลงอย่างมาก การจัดการเหตุการณ์ก็ถูกรวมศูนย์เช่นกัน ตอนนี้ถูกจัดการโดยตัวจัดการระดับบนสุดตัวเดียวโดยใช้ค่า data-attribute แทนที่ตัวจัดการเหตุการณ์แต่ละรายการจำนวนมากของ v1 แนวทางนี้ช่วยลดความซับซ้อนของโค้ดและเพิ่มประสิทธิภาพได้อย่างมาก

การจัดการสถานะอัจฉริยะและการเข้าถึงข้อมูลแบบ O(1)

บางทีการเปลี่ยนแปลงที่มีผลกระทบมากที่สุดคือการย้ายสถานะแอปที่ซับซ้อน เช่น การแสดงความคิดเห็นและเมนูบริบท ไปยังคอมโพเนนต์ลูกที่เรนเดอร์แบบมีเงื่อนไข ในสภาพแวดล้อมเช่น GitHub ที่พูลรีเควสต์สามารถมีได้มากกว่าหลายพันบรรทัด การที่ทุกบรรทัดต้องมีสถานะการแสดงความคิดเห็นที่ซับซ้อนเป็นสิ่งที่ไม่ประสิทธิภาพเมื่อมีเพียงส่วนน้อยเท่านั้นที่จะมีความคิดเห็น โดยการย้ายสถานะนี้ไปยังคอมโพเนนต์ที่ซ้อนกัน ความรับผิดชอบหลักของคอมโพเนนต์บรรทัด diff จึงกลายเป็นการเรนเดอร์โค้ดอย่างแท้จริง ซึ่งสอดคล้องกับหลักการความรับผิดชอบเดียว (Single Responsibility Principle)

นอกจากนี้ v2 ยังแก้ไขปัญหาการค้นหาแบบ O(n) และ useEffect hooks ที่มากเกินไปซึ่งเป็นปัญหาของ v1 ทีมงานได้นำกลยุทธ์สองส่วนมาใช้: จำกัดการใช้ useEffect อย่างเข้มงวดในระดับบนสุดของไฟล์ diff และกำหนดกฎการ linting เพื่อป้องกันการนำกลับมาใช้ใหม่ในคอมโพเนนต์ที่ห่อหุ้มบรรทัด สิ่งนี้ช่วยให้มั่นใจถึงการ memoization ที่แม่นยำและพฤติกรรมที่คาดเดาได้ ในขณะเดียวกัน เครื่องสถานะส่วนกลางและ diff ถูกออกแบบใหม่เพื่อใช้ประโยชน์จากการค้นหาแบบ O(1) (เวลาคงที่) โดยใช้อ็อบเจกต์ JavaScript Map สิ่งนี้ช่วยให้สามารถใช้ selectors ที่รวดเร็วและสอดคล้องกันสำหรับการดำเนินการทั่วไป เช่น การเลือกบรรทัดและการจัดการความคิดเห็น ซึ่งช่วยเพิ่มคุณภาพโค้ด ปรับปรุงประสิทธิภาพ และลดความซับซ้อนได้อย่างมากโดยการรักษาโครงสร้างข้อมูลที่ราบเรียบและถูกแมป แนวทางที่พิถีพิถันนี้ในการ เพิ่มประสิทธิภาพเวิร์กโฟลว์ของนักพัฒนา และสถาปัตยกรรมพื้นฐานช่วยให้มั่นใจได้ถึงระบบที่แข็งแกร่งและปรับขนาดได้

ผลกระทบที่วัดผลได้: V2 นำมาซึ่งผลกำไรที่วัดผลได้

การเพิ่มประสิทธิภาพทางสถาปัตยกรรมและระดับโค้ดที่พิถีพิถันซึ่งนำมาใช้ใน v2 ได้สร้างการปรับปรุงที่ลึกซึ้งและวัดผลได้ในเมตริกประสิทธิภาพหลัก ระบบใหม่ทำงานได้เร็วขึ้นอย่างมาก พร้อมการลดการใช้ฮีป JavaScript และคะแนน INP ลงอย่างมหาศาล ตารางต่อไปนี้แสดงให้เห็นถึงการปรับปรุงที่น่าทึ่งที่สังเกตได้ในพูลรีเควสต์ตัวอย่างที่มีการเปลี่ยนแปลง 10,000 บรรทัดในการตั้งค่า split diff:

เมตริกv1v2การปรับปรุง
ฮีป JavaScript1GB+250MB75%
โหนด DOM400,000+80,00080%
INP p951000ms+100ms90%

ตัวเลขเหล่านี้เน้นย้ำถึงความสำเร็จของกลยุทธ์หลายด้านของ GitHub การลดขนาดฮีป JavaScript 75% และการลดโหนด DOM 80% ไม่เพียงหมายถึงการใช้ทรัพยากรเบราว์เซอร์ที่เบาลงเท่านั้น แต่ยังส่งผลโดยตรงต่ออินเทอร์เฟซที่เสถียรและตอบสนองได้ดีขึ้น การปรับปรุงที่น่าทึ่งที่สุดคือ การลด INP p95 (เปอร์เซ็นไทล์ที่ 95 ของความล่าช้าในการโต้ตอบ) ลง 90% หมายความว่าตอนนี้ 95% ของการโต้ตอบของผู้ใช้เสร็จสมบูรณ์ภายในเวลาเพียง 100 มิลลิวินาที ซึ่งแทบจะกำจัดความล่าช้าในการป้อนข้อมูลที่เคยเป็นปัญหาในพูลรีเควสต์ขนาดใหญ่ใน v1 สิ่งนี้ช่วยเพิ่มประสบการณ์ผู้ใช้อย่างมีนัยสำคัญ ทำให้การตรวจสอบโค้ดขนาดใหญ่รู้สึกราบรื่นและตอบสนองได้ดีเหมือนกับการตรวจสอบขนาดเล็ก

ความมุ่งมั่นของ GitHub ในการปรับปรุงอย่างต่อเนื่อง ซึ่งเห็นได้จากการเจาะลึกการเพิ่มประสิทธิภาพบรรทัด diff นี้ เป็นเครื่องพิสูจน์ถึงความทุ่มเทของพวกเขาในการจัดหาแพลตฟอร์มสำหรับนักพัฒนาระดับโลก ด้วยการวิเคราะห์คอขวดด้านประสิทธิภาพอย่างเข้มงวดและการนำโซลูชันทางสถาปัตยกรรมที่กำหนดเป้าหมายมาใช้ พวกเขาไม่เพียงแก้ไขปัญหาความสามารถในการปรับขนาดที่สำคัญเท่านั้น แต่ยังสร้างมาตรฐานใหม่สำหรับการตอบสนองในผลิตภัณฑ์หลักของพวกเขาด้วย การมุ่งเน้นที่ประสิทธิภาพนี้ทำให้วิศวกรสามารถมีส่วนร่วมในงานที่สำคัญ เช่น การตรวจสอบโค้ดได้อย่างมีประสิทธิภาพ ซึ่งท้ายที่สุดจะนำไปสู่ คุณภาพและความปลอดภัยของโค้ด ที่สูงขึ้น และสภาพแวดล้อมการพัฒนาที่มีประสิทธิผลมากขึ้น

คำถามที่พบบ่อย

What is the 'Files changed' tab in GitHub pull requests and why was its performance critical?
The 'Files changed' tab is a core component of GitHub's pull request workflow, allowing engineers to review code modifications. Its performance is critical because it's where developers spend significant time, and slowdowns, especially with large pull requests, can severely impede productivity and user experience. GitHub prioritized its optimization to ensure responsiveness across all scales of code changes, from minor fixes to extensive refactorings, which can involve millions of lines across thousands of files. Maintaining a smooth and efficient review process is paramount for collaborative development.
What were the primary performance challenges GitHub faced with large pull requests in the v1 architecture?
In its initial React-based architecture (v1), GitHub encountered significant performance degradation when handling large pull requests. Key issues included the JavaScript heap exceeding 1 GB, DOM node counts soaring past 400,000, and page interactions becoming extremely sluggish or even unusable. The Interaction to Next Paint (INP) metric, which measures responsiveness, showed unacceptably high values. These problems stemmed from an inefficient rendering strategy where each diff line was resource-intensive, with too many DOM elements, React components, and event handlers, particularly in cases involving thousands of lines of code.
How did GitHub approach solving the complex performance issues, moving beyond a 'silver bullet' solution?
Recognizing that no single solution would address the diverse range of pull request sizes and complexities, GitHub adopted a multi-faceted strategic approach. They focused on three core themes: targeted optimizations for diff-line components to keep medium and large reviews fast, graceful degradation with virtualization to maintain usability for the largest pull requests by limiting rendered content, and investing in foundational components and rendering improvements to yield compounding benefits across all pull request sizes. This comprehensive strategy allowed them to tailor solutions to specific problem areas.
What were the key limitations of the 'v1' diff rendering architecture that made it unsustainable for scale?
The v1 architecture, while initially sensible for smaller diffs, proved unsustainable for large-scale pull requests. Each diff line was costly, requiring 10-15 DOM elements, 8-13 React components, and over 20 event handlers. This was compounded by deep component nesting, excessive `useEffect` hooks, and O(n) data lookups, leading to unnecessary re-renders and increased complexity. The abstraction layers, meant to share code, inadvertently added overhead by carrying logic for both split and unified views, even when only one was active. This design led to a significant increase in JavaScript heap, DOM count, and poor INP scores for larger diffs.
What specific architectural changes were implemented in 'v2' to drastically improve diff line performance?
The v2 architecture introduced several critical changes. It streamlined the component tree, reducing React components per diff line from eight to two by creating dedicated components for split and unified views, even with some code duplication. Event handling was centralized to a single top-level handler using `data-attribute` values, replacing numerous individual handlers. Complex app state, such as commenting features, was moved into conditionally rendered child components, ensuring that diff lines primarily focused on rendering code. Furthermore, v2 restricted `useEffect` hooks to top-level diff files and adopted O(1) constant-time data access using `JavaScript Map` for efficient state lookups, significantly reducing re-renders and improving data management.
How did the GitHub engineering team achieve quantifiable improvements in JavaScript heap, DOM nodes, and INP metrics with v2?
The cumulative effect of v2's architectural changes led to substantial quantifiable improvements. For a pull request with 10,000 line changes, the JavaScript heap size was reduced from 1GB+ to 250MB, a 75% improvement. DOM nodes decreased from 400,000+ to 80,000, an 80% reduction. The Interaction to Next Paint (INP) p95 (95th percentile) saw an astounding 90% improvement, dropping from 1000ms+ to just 100ms. These results were achieved through meticulous optimization, including removing extraneous DOM elements, simplifying the React component structure, centralizing event handling, and optimizing state management and data access patterns, leading to a much faster and more responsive user experience.
What is Interaction to Next Paint (INP) and why is its improvement significant for GitHub's user experience?
Interaction to Next Paint (INP) is a crucial web performance metric that assesses a page's responsiveness by measuring the latency of all interactions made by a user with the page. It records the time from when a user interacts (e.g., click, tap, keypress) until the next frame is painted to the screen, reflecting the visual feedback of that interaction. For GitHub, a high INP meant users experienced noticeable input lag, making the platform feel slow and unresponsive. By reducing INP p95 from over 1000ms to 100ms in v2, GitHub significantly enhanced the perceived speed and fluidity of the 'Files changed' tab, ensuring a smoother and more satisfying developer experience, especially during code review.

อัปเดตข่าวสาร

รับข่าว AI ล่าสุดในกล่องจดหมายของคุณ

แชร์