Tree Shaking React แอปเนทีฟ
วิธีที่เราใช้เทคนิคการเพิ่มประสิทธิภาพทั่วไปกับเว็บแอปกับแอป 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 (คุณลักษณะและเส้นทาง)
ภาพเหล่านี้สร้างขึ้นโดยstatoscope.tech ที่น่าทึ่ง ซึ่งช่วยเราวิเคราะห์การเปลี่ยนแปลงนี้และจะช่วยเราปรับปรุงขนาดบันเดิลต่อไป
โปรดทราบว่าการลดขนาดไม่ได้มาจากการลบการส่งออกที่ไม่ได้ใช้เท่านั้น แต่ยังทำให้ Webpack ModuleConcatenationPlugin
สามารถเชื่อมต่อโมดูลได้มากขึ้นด้วย กล่าวอีกนัยหนึ่ง เราสามารถกำหนดขอบเขตการยกโมดูลเพิ่มเติมได้ เรายังไม่ได้ใช้การยกขอบเขตอย่างเต็มที่ ตอนนี้มีเพียง 20% ของโมดูลเท่านั้นที่ถูกยกขึ้น เราคาดว่าขนาดของบันเดิลและรันไทม์จะเพิ่มขึ้นเมื่อเราเพิ่มจำนวนนั้น
การลดลง 40% ใน JavaScript จะแมปเกือบ 1:1 กับเวลาที่ใช้ในการประเมินชุด JavaScript ก่อนที่จะสามารถดำเนินการได้ การประเมิน JavaScript กำลังบล็อกเวลาเริ่มต้นที่เป็นผลลัพธ์ ดังนั้นการลดจำนวนของ JavaScript ที่จัดส่งโดยตรงจึงลดเวลาเริ่มต้นลง
หลังจากขั้นตอนสุดท้ายของการใช้งานเสร็จสิ้นในอีก 2 สัปดาห์ต่อมา เรายังคงมีผลลัพธ์เหมือนเดิมในห้องปฏิบัติการของเรา และพร้อมที่จะลงคุณสมบัตินี้ในสาขาหลักของเรา เราใช้ความระมัดระวังเป็นพิเศษในการลงจอดการเปลี่ยนแปลงขั้นสุดท้ายโดยตรงหลังจากที่เราหยุดเผยแพร่ สิ่งนี้ทำให้เราสามารถทดสอบระบบโมดูลใหม่ได้อย่างกว้างขวางในเวอร์ชันแอปภายในที่พนักงานของเราใช้ หลังจากหนึ่งสัปดาห์ของการทดสอบภายใน คุณลักษณะนี้ค่อยๆ ทยอยเปิดตัวแก่ผู้ใช้ปลายทางของเรา เรามีความหวังมากหลังจากที่เห็นว่าความเสถียรของแอปไม่ได้รับผลกระทบมากนัก ข้อมูลการผลิตแสดงให้เห็นการปรับปรุงสัมพัทธ์เดียวกันกับเวลาเริ่มต้นเฉลี่ยที่เราเห็นในผลการทดลองของเรา:
เวอร์ชัน 22.37 ไม่มีการสั่นสะเทือนของต้นไม้ 22.38 น. ต้นไม้สะเทือน
การปรับปรุงเหล่านี้มาพร้อมกับเวลาในการสร้างที่เพิ่มขึ้น การรวมบันเดิล 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เพื่อติดตามบทความเพิ่มเติมเช่นนี้