Tree Shaking React แอปเนทีฟ

Nov 28 2022
วิธีที่เราใช้เทคนิคการเพิ่มประสิทธิภาพทั่วไปกับเว็บแอปกับแอป React Native เพื่อลดเวลาเริ่มต้นลง 20% ต้นไม้อะไร? เป็นที่ยอมรับว่า Tree Shaking อาจเป็นคำที่สับสน

วิธีที่เราใช้เทคนิคการเพิ่มประสิทธิภาพทั่วไปกับเว็บแอปกับแอป React Native เพื่อลดเวลาเริ่มต้นลง 20%

ต้นไม้อะไร?

เป็นที่ยอมรับว่า Tree Shaking อาจเป็นคำที่สับสน คุณอาจเคยได้ยินชื่อนี้ในชื่อ "import elision" ใน TypeScript Tree Shaking เป็นรูปแบบหนึ่งของการกำจัด Dead-codeที่เกี่ยวข้องกับการลบการส่งออกที่ไม่ได้ใช้โดยเฉพาะ หากเราจะเชื่อมโมดูลทั้งหมดเข้าด้วยกัน การส่งออกที่ไม่ได้ใช้จะเป็นรหัสที่ใช้งานไม่ได้และสามารถลบออกได้ อย่างไรก็ตาม กระบวนการพิจารณาการส่งออกที่ไม่ได้ใช้นั้นไม่ใช่เรื่องเล็กน้อย โดยปกติแล้ว Tree Shaking จะถูกนำไปใช้ที่ระดับคอมไพเลอร์/บันเดิลเลอร์ (เช่นWebpackหรือESBuild ) ไม่ใช่โดยเอ็นจิ้น JavaScript (เช่น V8 หรือ Hermes) รูปแบบต่างๆ มากมายใน JavaScript สามารถทำลาย Tree Shaking ได้ แต่ในบทความนี้ ฉันต้องการเน้นที่ด้านเดียว: ระบบโมดูล ระบบโมดูลที่เกี่ยวข้องสองระบบที่เราต้องทำความเข้าใจที่นี่คือโมดูล CommonJSและโมดูล ES

CommonJS ใช้เมื่อคุณเขียนmodule.exports = {}หรือexports.someMethod = () => {}. โมดูล ES ถูกระบุโดยimportและexportไวยากรณ์ คอมไพเลอร์ใช้ Tree Shaking กับโค้ดโดยใช้ CommonJS ได้ยากกว่าโมดูล ES โมดูล CommonJS มักจะเป็นไดนามิกในขณะที่โมดูล ES สามารถวิเคราะห์แบบคงที่ได้ ตัวอย่างเช่น การตรวจหาตัวระบุการส่งออกทั้งหมดในโค้ดต่อไปนี้แบบคงที่ไม่ใช่เรื่องเล็กน้อย:

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

พื้นหลัง

ก่อนที่ฉันจะเข้าร่วม Klarna ฉันไม่มีประสบการณ์ในการทำงานกับ React Native เลย ในระหว่างการรีแฟคเตอร์ตามปกติ ฉันใช้ส่วนต่างต่อไปนี้:

สมมติว่าบันเดิลที่ใช้จะถือว่าsomeFeatureMethodไม่ได้ใช้หากSOME_STATIC_FLAGเป็นเท็จ และด้วยเหตุนี้จึงลบออกsome-feature-moduleจากบันเดิลสุดท้าย ในระหว่างการตรวจสอบโค้ด ความแตกต่างนี้ถูกทำเครื่องหมายว่าเป็นปัญหา ดังนั้นฉันจึงนั่งลงและตรวจสอบสมมติฐานของฉันอีกครั้งว่าสิ่งเหล่านี้เสียหายตรงไหน โชคดีที่เราได้เปลี่ยนมาใช้ Webpack (ในรูปแบบของRe.Pack ) เมื่อสองสามเดือนก่อนหน้านี้เพื่อเปิดใช้งานการแยกบันเดิลด้วยReact.lazy . สิ่งนี้ทำให้ฉันสามารถกำหนดค่ากระบวนการบันเดิลได้ง่ายขึ้น ด้วยวิธีที่ฉันสามารถตรวจสอบบันเดิล JavaScript สุดท้ายได้ เฉพาะHermes เท่านั้น ที่ต้องปิดการใช้งานในกรณีของเราเพื่อดูเอาต์พุต JavaScript ขั้นสุดท้าย

หลังจากลองผิดลองถูกเพื่อให้ง่ายต่อการค้นหาตำแหน่งที่some-feature-moduleนำเข้า ฉันพบบรรทัดต่อไปนี้: c=(n(463526),n(456189)ตัวดำเนินการลูกน้ำเป็นสิ่งที่คุณไม่ได้ใช้ ดังนั้นให้ฉันสรุปสิ่งนี้: มันประเมินตัวถูกดำเนินการทั้งหมดและใช้เฉพาะค่าส่งคืนของ ตัวดำเนินการสุดท้าย กล่าวอีกนัยหนึ่ง ค่าส่งคืนของn(463526)ไม่ได้ใช้ เนื่องจากฉันมีประสบการณ์ในการทำงานกับ tree-shaking บนเว็บอยู่แล้ว จึงค่อนข้างชัดเจนสำหรับฉันว่าสิ่งนี้คืออะไรก่อนที่จะมีการลดขนาด: require('some-feature-module')(Webpack แปลงสตริงต้นทางการนำเข้าเป็นตัวเลข)

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

สิ่งที่เราต้องทำเพื่อให้ส่วนต่างเดิมทำงานได้ตามที่คาดไว้คือต้องแน่ใจว่า Tree Shaking มีผลกับบันเดิลสุดท้าย

การดำเนินการ

ทั้งหมดลงมาเพื่อให้แน่ใจว่าคุณไม่ได้แปลงโมดูล ES เป็น CommonJS ก่อนที่ Webpack จะรวมโมดูลทั้งหมด หากคุณใช้ ค่าที่ ตั้งไว้ล่วงหน้าของ Metro Babel (ค่าเริ่มต้นสำหรับแอป React Native ใหม่) งานส่วนใหญ่จะอยู่ที่การเปิดใช้งานdisableImportExportTransform:

โปรดทราบว่าขณะนี้ตัวเลือกนี้ยังไม่มีเอกสารและอาจถูกลบออกได้ทุกเมื่อ

เรายังจำเป็นต้องบอก Webpack ให้ใช้จุดเข้าใช้งานที่ใช้โมดูล ES แทนโมดูล CommonJS สำหรับแต่ละไฟล์นั่นหมายถึงการเลือก.mjsในขณะที่สำหรับแพ็คเกจ เราจำเป็นต้องบอกให้ Webpack ใช้moduleฟิลด์หลัก

อย่างไรก็ตาม สิ่งนี้เผยให้เห็นปัญหาเกี่ยวกับวิธีที่เราเขียน JavaScript และวิธีการเขียนโค้ดในระบบนิเวศของ React Native เราได้ระบุปัญหา 3 ประเภท

การส่งออกไวยากรณ์ที่แตกต่างกันในmainและmodule

ฟิลด์หลักเหล่านี้ควรใช้เพื่อแยกความแตกต่างของระบบโมดูลเท่านั้น ( mainสำหรับ CommonJS moduleสำหรับโมดูล ES) อย่างไรก็ตาม แพ็คเกจจำนวนมากจัดส่งไวยากรณ์ที่ทันสมัยกว่าจากmoduleจุดเริ่มต้น ตัวอย่างเช่นclassปัจจุบัน Hermes ไม่รองรับไวยากรณ์

สำหรับตอนนี้ เราแปลnode_modulesเนื้อหาทั้งหมดลงไปเป็นไวยากรณ์ ES5 หรือมากกว่าไวยากรณ์ที่ Hermes รองรับโดยการเพิ่มแบบกำหนดเอง ในการกำหนด ruleค่า Webpack:

การนำเข้าโมดูล CommonJS ด้วยไวยากรณ์ที่ไม่ชัดเจน

Webpack จะไม่สามารถค้นหาการส่งออกจากโมดูลที่ผสมระบบโมดูลได้ อย่างไรก็ตาม React Native จัดส่งไฟล์ต้นฉบับด้วยระบบโมดูลแบบผสม เช่น

วิธีแก้ไขที่นี่คือการแปลงโมดูลเหล่านี้ไปยัง CommonJS ต่อไป (ซึ่งก็คือการปิดใช้งาน Tree Shaking) โดยเพิ่มส่วนพิเศษruleให้กับ Webpack config:

ไม่มีตัวระบุการนำเข้า

นี่เป็น SyntaxError ใน JavaScript ที่คนส่วนใหญ่ไม่รู้จัก ตัวอย่างเช่นimport { doesNotExist } from 'some-module';จะโยนSyntaxError. สร้างความรำคาญให้กับนักพัฒนาอย่างมาก แต่อาจนำไปสู่ปัญหารันไทม์ที่แท้จริงได้ เราบังคับใช้โมดูล ES อย่างเข้มงวดนี้ใน Webpack โดยเปิดใช้งานmodule.parser.javascript.exportsPresenceในการกำหนดค่า Webpack

ปัญหาเหล่านี้ส่วนใหญ่เกิดจากการรีพอร์ตประเภทใน TypeScript เช่น

โชคดีที่ TypeScript สามารถตั้งค่าสถานะปัญหาเหล่านี้ในระดับประเภทโดยเปิดใช้งานisolatedModules:

typeตัวแก้ไขชื่อนำเข้าเป็นสิ่งใหม่ใน TypeScript 4.5 การเพิ่มการรองรับtypeตัวแก้ไขในชื่อนำเข้าค่อนข้างท้าทาย เนื่องจากเราจำเป็นต้องอัปเกรดตัวแยกวิเคราะห์ ESLint ที่เราใช้ , Prettierและ TypeScript

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

ผลลัพธ์

การใช้งานครั้งแรกค่อนข้างแฮ็ค อย่างไรก็ตาม ผลลัพธ์แรกแสดงให้เห็นการปรับปรุงการเริ่มต้นเฉลี่ย 20% ของทั้งสองแพลตฟอร์ม (Samsung Galaxy S9 2.2s ลดลงจาก 2.8s และ iPhone 11 640ms ลดลงจาก 802ms)

สิ่งที่เราเห็นคือการลดลง 46% ของจาวาสคริปต์เริ่มต้นที่สำคัญของเรา ขนาดโดยรวมของ JavaScript ที่เราจัดส่งลดลง 14% ความแตกต่างส่วนใหญ่เกิดจากการย้ายโค้ดจากกลุ่มหลักไปยังกลุ่ม async (คุณลักษณะและเส้นทาง)

ค่าความแตกต่างของค่าสถานะกลุ่มหลังจาก Tree Shaking
ค่าความต่างสัมพัทธ์ของสถิติกลุ่มหลังจาก Tree Shaking

ภาพเหล่านี้สร้างขึ้นโดยstatoscope.tech ที่น่าทึ่ง ซึ่งช่วยเราวิเคราะห์การเปลี่ยนแปลงนี้และจะช่วยเราปรับปรุงขนาดบันเดิลต่อไป

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

การลดลง 40% ใน JavaScript จะแมปเกือบ 1:1 กับเวลาที่ใช้ในการประเมินชุด JavaScript ก่อนที่จะสามารถดำเนินการได้ การประเมิน JavaScript กำลังบล็อกเวลาเริ่มต้นที่เป็นผลลัพธ์ ดังนั้นการลดจำนวนของ JavaScript ที่จัดส่งโดยตรงจึงลดเวลาเริ่มต้นลง

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

เวอร์ชัน 22.37 ไม่มีการสั่นสะเทือนของต้นไม้ 22.38 น. ต้นไม้สะเทือน

ข้อมูลการผลิตสำหรับผู้ใช้ Android
ข้อมูลการผลิตสำหรับผู้ใช้ iOS

การปรับปรุงเหล่านี้มาพร้อมกับเวลาในการสร้างที่เพิ่มขึ้น การรวมบันเดิล JavaScript ที่ใช้งานจริงจะใช้เวลาเพิ่มขึ้นประมาณ 30% (4 นาที) เราใช้เวลาในการสร้างที่เพิ่มขึ้นเหล่านี้ด้วยความยินดี เนื่องจากเวลาเหล่านี้แปลตรงตัวเพื่อประสบการณ์การใช้งานที่ดีขึ้น เวลาในการสร้างที่เพิ่มขึ้นบางส่วนเกิดจากการขนถ่ายมากกว่าที่เราต้องการ การใช้งานครั้งแรกไม่ได้ใช้เวลาในการลดจำนวนที่จำเป็นในการทรานสไพล์แต่อย่างใด นอกจากนี้ เราจะชดเชยเวลาการสร้างที่เพิ่มขึ้นบางส่วน ยิ่งมีแพ็คเกจมากเท่าใด ก็จะส่งโมดูล ES ที่เหมาะสม โปรดทราบว่าเวลารวม JavaScript ไม่ใช่งานเดียวที่จำเป็นสำหรับการสร้างแอป React Native ด้วยการคอมไพล์ไบนารี ฯลฯ การเพิ่มขึ้นของการรวม JavaScript นั้นไม่ได้ส่งผลกระทบในท้ายที่สุด

อะไรต่อไป

ดูเหมือนว่าโมดูล ES ยังไม่ได้ทำงานในระบบนิเวศของ React Native เราต้องการจัดแนวระบบนิเวศให้มากขึ้นเกี่ยวกับการใช้โมดูล ES อย่างเหมาะสม (เช่นmoduleรายการที่ชี้ไปที่ JavaScript ด้วยไวยากรณ์ที่เทียบเท่ากัน) ด้วยวิธีนี้เราสามารถลดการกำหนดค่าบิลด์และทรานสไพล์ให้น้อยลงได้

แม้ว่าจะมีการสนับสนุนสำหรับการใช้โมดูล ES หลังแฟล็กใน Metro ( experimentalImportSupport) โมดูลก็ถูกทำเครื่องหมายว่าเป็นการทดลองและไม่ได้บันทึกไว้ การเปิดใช้งานการตั้งค่าสถานะนั้นในการพัฒนาไม่ได้ผลสำหรับเรา (แต่) แต่เราหวังว่าสักวันหนึ่งเราจะสามารถใช้ระบบโมดูลเดียวกันในการพัฒนาและการผลิตได้ เราต้องการเริ่มการสนทนาเกี่ยวกับโมดูล ES ใน React native เนื่องจากดูเหมือนว่าการรองรับโมดูล ES นั้นยังไม่ได้ดำเนินการอยู่ การสนับสนุน Tree Shaking ถูกยกเลิกไปเมื่อหลายปีก่อน .

ท้ายที่สุดแล้ว โมดูล ES เป็นคุณสมบัติภาษาที่ทุกคนรู้จัก JavaScript ก็เรียนรู้ในที่สุดเช่นกัน เราไม่เห็นเหตุผลว่าทำไม React Native ควรมีขั้นตอนการเรียนรู้เพิ่มเติมเพื่อทำความเข้าใจการแยกบันเดิลและการกำจัดโค้ดที่ตายแล้ว

เขียนครั้งเดียว วิ่งได้ทุกที่!

คุณสนุกกับโพสต์นี้หรือไม่? ติดตาม Klarna Engineering บนสื่อกลางและLinkedInเพื่อติดตามบทความเพิ่มเติมเช่นนี้