React Native - Hermes JavaScript Engine ตัวใหม่จาก Facebook

React Native - Hermes JavaScript Engine ตัวใหม่จาก Facebook
16/09/19   |   7.2k

Brief

Hermes คือ open-sourced JavaScript engine ตัวใหม่ที่ถูกพัฒนาโดย Facebook และนำมาใช้ใน React Native (เฉพาะ android) แทนการใช้ JavaScriptCore (Safari WebKit) เพื่อให้ User experience ในการใช้งานในมือถือดีขึ้น ผลลัพธ์ที่เกิดขึ้นได้แก่

  1. การใช้งาน Memory (RAM) ลดลง
  2. ขนาดของไฟล์ apk ลดลง
  3. เวลาที่ใช้ใน Startup แอพดีขึ้น (Time to interactive - TTI ดีขึ้น)


Ref image จาก https://engineering.fb.com/android/hermes/


Background

ก่อนอื่นต้องเล่าก่อนว่าทำไมถึงต้องมี Hermes

เนื่องจากว่าการพัฒนา React Native นั้นพุ่งเป้าไปใช้งานในมือถือเป็นหลัก และเนื่องจากมือถือส่วนใหญ่ตามท้องตลาดไม่ได้มีสเปกที่สูงมาก (RAM + CPU ไม่ได้สูงมาก) หากนำมาเปิด React Native App ของเราก็อาจเปิดขึ้นมาช้ามากกกในจังหวะแรก และมีโอกาสสูงที่ App ของเราจะถูก Kill บ่อยๆด้วย OS เพราะ Memory Usage สูงนั่นเอง หรือในบางกรณี User อาจไม่อยากลง App ของเราเพราะขนาดไฟล์ที่ใหญ่โตทำให้ต้องลบ App อื่น เพื่อให้มีพื้นที่ว่างพอ หรือไม่คุ้มพอสำหรับอินเทอร์เน็ทที่ต้องใช้ในการลง App ของเรา


ท้ายที่สุด Facebook ผู้ Support React Native ของเรา ก็ออกมาทำ Open-Sourced JavaScript engine เองใน Android แทนการใช้งาน JavaScriptCore ซึ่งเป็นกำลังหลักใน Safari Webkit ที่ถูกใช้ใน React Native มายาวนาน โดย JavaScript engine ตัวใหม่นี้มีชื่อว่า Hermes โดยมีเป้าหมายคือเพิ่มประสิทธิภาพให้กับ React Native App โดยเฉพาะเพื่อขจัดปัญหาที่ได้กล่าวมาในข้างต้นให้หมดไป


Note:

JavaScriptCore สำหรับ iOS จะสามารถเรียกผ่าน OS ใน Safari Webkit ได้เลย แต่ใน Android นั้นไฟล์ .apk ของเราจะต้องเป็นคนแบก JavaScriptCore ไปด้วย โดยหาก Build แบบ Universal apk ก็จะทำให้มี CPU Architecture ของทุกประเภทที่ Support ติดไปด้วย และขนาดไฟล์เหล่านี้มีขนาดใหญ่มาก หากต้องการลดขนาดจะต้อง Build แบบแยก .apk เป็นหลายไฟล์ตาม CPU Architecture 

ไฟล์ JavaScriptCore ที่ .apk นำไปด้วย โดยมีทุก Architecture ที่ Support ทำให้ไฟล์ค่อนข้างใหญ่



How it works

เราจะมาดูกันว่า Hermes Improve ประสิทธิภาพของ (Android) React Native ได้อย่างไร


สร้าง Bytecode ในขั้นตอนการ Build App

    ตามปกติแล้วใน React Native หากเราทำการแงะไฟล์ Apk ที่ได้จากการ Build ออกมาดู จะพบกับไฟล์ index.android.bundle ซึ่งเป็น bundle ที่รวม JavaScript Source Code ของเราทั้ง Project เอาไว้ในไฟล์เดียวนี้ ซึ่งก็จะเกี่ยวโยงไปถึงการทำงานของ JavaScriptCore ที่จะ Parse ไฟล์นี้ทุกครั้งที่เรากดเปิด App เพื่อแปลงไฟล์นี้ให้กลายเป็น Bytecode

ซึ่งขั้นตอนการ Parse JS to Bytecode นี้ทำให้การเปิด App ทุกครั้งนั้นช้า เพราะต้องรอกระบวนการนี้เสร็จ ถึงจะได้ไป Run Source Code ของแอพเราจริงๆ

ไฟล์ index.android.bundle ใน .apk ที่ถูก Build จาก React Native


    เพื่อลดขั้นตอนนี้ Hermes ตัดสินใจนำ AOT (ahead-of-time compiler) มาใช้แทน JIT (Just-in-time compiler) ซึ่ง AOT จะทำการ Parse JS ให้เป็น ByteCode ให้เสร็จในขั้นตอนการ Build Apk เลย

นั่นส่งผลให้ประสิทธิภาพของ ByteCode ของเราดีขึ้นและขนาดเล็กลงมาก เพราะเราจะสามารถทำ Optimization และใช้เวลานานเท่าไหนก็ได้ในขั้นตอนการ Build Apk โดยต่างจากเดิมที่ต้องทำหน้างานตอนกดเปิด App ขึ้นมาเท่านั้น

    โดย Bytecode ที่ได้จาก Hermes ถูกออกแบบมาให้ Map เข้า RAM และ Interpret ได้โดยไม่ต้องอ่านไฟล์ทั้งหมดแล้วเหมือนแต่ก่อน เพราะการทำ Memory I/O นั้นค่อนข้างช้ามากในมือถือระดับล่าง ทำให้ Hermes นั้นรีดความเร็วในการเปิด App ได้ดียิ่งขึ้น และโดยเฉพาะอย่างยิ่งกับ Android ที่ไม่มีการทำ Swap Memory ก็จะไม่ค่อยเจอ "Out of Memory" ในกรณีที่ใช้ RAM เกินอีกด้วย


Ref image จาก https://engineering.fb.com/android/hermes/


ไม่ใช้ JIT (Just-in-time compiler) แล้วมันคืออะไร

โดยปกติแล้ว JavaScriptCore นั้นจะ Run JS Virtual Machine ขึ้นมา โดยจะ parse .js source code ให้เป็น Bytecode แล้วค่อยเป็น Machine Code (Native Code) อีกที ซึ่งใน Concept "write once run anywhere" ที่คุ้นหูของ Java ก็มาจาก Bytecode ที่เป็นภาษากลางใน Java (intermediate code) แล้วค่อยให้ Interpreter ของแต่ละ CPU Architecure นำไป Interpret ให้เป็น Machine Code เอาเอง


ตามปกติ Java/JavaScript Bytecode จะมี 3 Options ในการถูก Executed คือ
  1. Interpret Bytecode ใน JS Virtual Machine ตอน Runtime เลย
  2. Compile Bytecode เฉพาะบรรทัด/ส่วนที่จะใช้งานตอน Runtime เลย (ก่อนที่จะ Execute), ให้เป็น Machine Code แล้วค่อย Execute วิธีนี้ถูกเรียกว่า JIT (Just-in-time compiler)
  3. Compile Bytecode ทั้งหมดให้เป็น Machine Code ตั้งแต่แรกก่อนเปิดโปรแกรมเลย วิธีนี้ถูกเรียกว่า AOT (Ahead-of-time compiler)

สำหรับ JavaScriptCore ใน Android จะใช้แบบผสมคือ 1,2 คือทำการ Interpret Bytecode (ที่ parse มาจาก JS Code) แล้วส่วนไหนที่ถูกเรียกใช้งานบ่อยๆก็จะนำ JIT เข้ามาแปลงให้เป็น Machine Code เพื่อ Optimize ความรวดเร็วในการ Execute นั่นเอง

สำหรับ iOS นั้นไม่สามารถทำ JIT (ข้อ 2) ได้ เพราะ iOS ไม่ยอมให้เขียนหรือ Execute ข้อมูลใน Memory ได้ จึงทำให้ JIT ใช้ได้แค่ใน Android

โดย Hermes ก็จะเป็นการใช้งานในข้อ 3 คือใช้ AOT ตั้งแต่การ Build App ข้อดีก็จะเป็นไปตามหัวข้อก่อนหน้าที่ได้กล่าวไว้ ส่วนข้อเสียก็คือจะ Optimize ได้ไม่ค่อยดีนัก เนื่องจากปกติ JIT จะ Optimize หน้างานเลย ทำให้รู้ว่าตัว var/const/let ชนิดนี้เป็น Type ไหน มีการเข้าบรรทัดไหนบ้าง แต่ AOT ก็จะทำ Optimize ในส่วนนี้และส่วนอื่นๆได้ไม่ดีนัก เพราะไม่อาจรู้ล่วงหน้าได้จนกว่าจะถึงหน้างานจริง ทำให้ Benchmarks ของ App นั้น underperforms ในการรีด CPU มาใช้งาน


illustration by Ouch.pics https://icons8.com


สำหรับข้อเสียนี้ ก็ยอมรับได้เพราะ CPU Benchmarks ไม่ได้แสดงถึง workloads การทำงานที่แท้จริงใน Mobile Application และในทางตรงกันข้าม JIT ก็มีข้อเสีย เช่น
  1. ใช้เวลาในการ startup (warm up) นานในทุกครั้งที่เปิดแอพ ซึ่งเป็นข้อเสียที่ทำให้ TTI (Time to interactive) มีขนาดใหญ่
  2. ขนาดไฟล์ Apk มีขนาดใหญ่เพราะต้องนำ Compiler ติดไปด้วยและต้องมีไฟล์ทุก CPU Architecture
  3. ใช้ Memory ค่อนข้างเยอะเพราะต้องทำการ Cache Code เก็บเอาไว้ Execute ทีหลัง

ซึ่งนั่นก็เป็นข้อเสียอย่างมากกับ Mobile Application มากกว่า CPU Benchmarks ซึ่งก็เป็น Trade-off ที่ Hermes ยอมรับได้ และนำ AOT มาใช้งานเพื่อลดข้อเสียเหล่านี้ออกไป


Garbage collector

นอกจากนี้ Hermes ยังได้ปรับแต่ง Garbage collector ให้มีประสิทธิภาพ โดยในอุปกรณ์มือถือนั้น RAM ค่อนข้างมีอยู่อย่างจำกัด และการทำ Swap นั้นจะไม่มีในมือถือ Low-End เพราะ Flash I/O นั้นค่อนข้างช้าและมีจำกัด ด้วยเหตุนี้เองทำให้ Applicaition โดน Kill อยู่บ่อยครั้ง และยังพบอีกว่า virtual address (VA) โดยเฉพาะอย่างยิ่งกับ VA ที่อยู่ติดกัน (contiguous) นั้นมีอยู่อย่างจำกัดในมือถือ 32-bits

illustration by Ouch.pics https://icons8.com


โดย Hermes ได้ปรับแต่ง Garbage collector ดังนี้

  • On-demand allocation: จอง VA Chunks เฉพาะตอนที่ใช้งานเท่านั้น
  • Noncontiguous: VA space ที่จะใช้ต้องไม่เป็น single memory range
  • Moving: สามารถย้าย Objects ได้นั้นทำให้ Memory สามารถทำ defragmented และคืน VA Chunks ที่ไม่ใช้งานกลับไปให้ OS ได้
  • Generational: จะไม่ Scan Javascript heap ทั้งหมด ในทุกครั้งที่เกิด Garbage collection ทำให้เวลาที่ใช้ลดลง


Migration

หากอ่านแล้วสนใจใช้งาน Hermes ก็จะต้องไปเปิดใช้งานด้วยตัวเอง (ไม่ใช่ Default ใน React Native) ตามขั้นตอนด้านล่างนี้เลยครับ
  1. React Native 0.60.2 ขึ้นไป
    (สำหรับการ Upgrade จาก version เก่า Link)
  2. เข้าไปแก้ไขไฟล์ android/app/build.gradle โดยเปลี่ยน enableHermes ให้เป็น true
    project.ext.react = [
    entryFile: "index.js",
    enableHermes: true
    ]
  3. Clean android project ด้วยคำสั่งนี้ 
    cd android && ./gradlew clean
  4. ทดสอบในโค้ดว่า Hermes ถูกใช้งานแล้วด้วยคำสั่ง
    const isHermes = () => global.HermesInternal != null;



References

  1. Hermes An open-source JavaScript engine optimized for mobile apps, starting with React Native: https://engineering.fb.com/android/hermes/
  2. Using Hermes: https://facebook.github.io/react-native/docs/hermes/


tags : mobile application react native



ติดตามข่าวสารและเรื่องราวดีๆ ทาง Email