React Native tại Airbnb (P2): Công nghệ

111

Xem Phần 1React Native tại Airbnb (P1)

React Native là một platform tương đối mới trên các cross-section của Android, iOS, web và các framework cross-platform. Sau 2 năm, chúng tôi có thể nói rằng React Native là một bước đột phá ở nhiều khía cạnh. Đây là một bước đột phá mới về thuật toán cho mobile và chúng ta có thể được lợi từ rất nhiều hoạt động. Tuy nhiên, lợi ích thu được sẽ đi kèm với không ít “thương tích”.

Những mặt hiệu quả

Cross-Platform

Lợi ích rõ ràng nhất của React Native đó là code bạn viết có thể chạy trên cả Android và iOS. Hầu hết các feature sử dụng React Native có thể đạt từ 95–100% shared code và 0.2% file chuyên cho platform (*.android.js/*.ios.js).

Design Language System thống nhất (DLS)

Chúng ta đã develop được một design language gọi là DLS. Chúng tôi làm các bản Android, iOS, React Native, và bản web cho mỗi component. Việc này hỗ trợ cho việc viết các feature cross-platform hơn vì mọi design, tên component và các screen thống nhất trên các platform. Tuy nhiên, chúng ta vẫn có thể đưa ra các quyết định phù hợp với platform. Ví dụ, chúng ta có thể dùng Toolbar trên Android và UINavigationBar trên iOS và chọn hide disclosure indicators trên Android vì họ không follow các design guideline của platform Android .

Chúng tôi đã viết lại các component thay vì giữ cái cũ để tạo ra được các API phù hợp cho từng platform và giảm thiểu workload cho các kĩ sư Android và iOS nào không rõ cách test các thay đổi trong React Native. Tuy nhiên, nó sẽ gây ra sự tách rời giữa các platform mà các phiên bản native và React Native của cùng component sẽ không sync với nhau nữa.

React

Chuyện React là một trong những web framework được ưa thích nhất là có lí do. Đơn giản mà quyền lực và dễ scale các codebase lớn hơn. Có một vài thứ mà chúng tôi đặc biệt ưa thích:

  • Các component: Các React Component giải quyết được nhiều khuất mắt bằng nhiều prop và state tốt.
  • Lifecycle đơn giản: Lifecycle của Android, và một extent bé hơn, của iOS rất phức tạp. Các component React căn bản sẽ xử lý vấn đề và làm cho việc học React Native dễ hơn nhiều so với việc học Android hoặc iOS.
  • Rõ ràng: Bản chất rõ ràng của React sẽ giúp đồng bộ UI in sync bằng các nguyên lý ngầm.

Iteration Speed

Khi develop React Native, chúng tôi có thể dùng hot reloading để test các thay đổi trên Android và iOS trong chỉ một hai giây. Mặc dù việc build performance là ưu tiên hàng đầu trong các app native, nhưng vẫn chưa có cái nào đạt được iteration speed mà chúng tôi đạt được nhờ React Native. Hơn thế nữa, thời gian kết hợp native là 15 giây tuy nhiên nó có thể lên đến 20 phút đối với full builds.

Đầu tư vào Infrastructure

Chúng tôi đã phát triển các hệ integration khủng vào infrastructure native của mình. Mọi mảng cốt yếu như networking, i18n, thí nghiệm, di chuyển các element chung, info của device, info của account, và nhiều thứ khác nữa trong một React Native API đơn. Các bridge này là các mảng khá phức tạp vì chúng ta muốn gói gọn các API Android và iOS vào một cái gì đó thống nhất và hợp lý cho React. Việc liên tục làm mới các bridge này nhanh chóng và phát triển thêm infrastructure mới là một cuộc chiến không ngừng nghỉ, vì thế mà việc đầu tư vào infrastructure team sẽ thúc đẩy được tiến trình tạo nên product.

Sự thiếu đầu tư vào infrastructure sẽ dẫn đến việc React Native mang lại những trải nghiệm tồi tệ cho developer và người dùng. Thế mới nói, React Native không thể nào kết hợp với app một cách êm đẹp nếu thiếu sự đầu tư liên tục và hợp lý.

Performance

Một trong những cái chúng tôi lo lắng nhất về React Native hiện tại đó là performance. Tuy nhiên, trên thực tế đó cũng không phải là vấn đề to tát lắm. Hầu hết các React Native screen chạy trơn tru như những cái của native. Performance thường được xem như một một mảng riêng. Chúng ta thường thấy các kĩ sư mobile nhìn vào JS và nghĩ rằng nó “chậm hơn Java”. Tuy nhiên, layout sẽ cải thiện được performance trong nhiều trường hợp.

Khi chúng ta thấy những vấn đề liên quan đến performance, thường chúng sẽ xuất phát từ việc render quá tải và được giảm tải bằng shouldComponentUpdateremoveClippedSubviews, cũng như sử dụng Redux hợp lí hơn.

Tuy nhiên, thời gian render lần đầu và khởi động làm cho React Native hoạt động không tốt khi launch screen, deeplink và tăng thời gian TTI khi đang định vị giữa các screen. Ngoài ra, các screen drop frame thì rất khó để debug vì Yoga dịch giữa các React Native component và các native view.

Redux

Chúng tôi dùng Redux để quản lý state khá hiệu quả và ngăn chặn UI không đồng bộ với state và cho phép việc chia sẻ data dễ dàng hơn giữa các screen. Tuy nhiên, Redux nổi tiếng về boilerplate của nó và hơi khó tiếp thu. Chúng tôi đã cung cấp một generator cho một số template phổ biến nhất định nhưng nó vẫn là một trong những phần khó và gây bối rối nhất khi làm việc với React Native. Nên lưu ý một điều là, không phải chỉ với React Native bạn mới gặp những thử thách như vậy.

Được hỗ trợ bởi Native

Vì mọi thứ trên React Native được bridge bằng native code, chúng ta có thể build nên rất nhiều thứ mà chúng ta không chắc là có thể từ đầu như:

  1. Shared element transitions: Chúng tôi đã build <SharedElement> component được hỗ trợ bởi native shared element code trên Android và iOS. Nó còn hoạt động được giữa screen native và screen React Native.
  2. Lottie: Chúng tôi đã thành công trong việc đưa Lottie hoạt động trên React Native bằng cách wrap các library có sẵn trên Android và iOS.
  3. Native networking stack: React Native sử dụng existing native networking stack và cache trên cả hai platform.
  4. Các core infra khác: Tương tự như networking, chúng tôi wrap phần native infrastructure còn lại như i18n, experimentation, v.v để nó hoạt dộng được trơn tru trên React Native.

Phân tích số liệu 

Chúng tôi có một bề dày lịch sử dùng eslint trên web rất hùng mạnh. Tuy nhiên, chúng tôi là platform đầu tiên tại Airbnb tiên phong Prettier. Chúng tôi nhận thấy rằng việc giảm nit và bikeshedding trên các PR rất hiệu quả. Prettier giờ đây được tích cực đầu tư bởi team web infrastructure.

Chúng tôi cũng dùng analytics để đo lường thời gian render và performance để tìm ra các screen nào cần được ưu tiên đầu tư hàng đầu về những vấn đề liên quan đến performance.

Vì React Native còn mới hơn và trẻ hơn so với web infrastructure của chúng tôi, nó sẽ là một vùng test idea mới hoàn hảo. Đã có rất nhiều tool và ý tưởng chúng tôi đưa ra hiện đã được show và sử dụng trên web.

Animation

Nhờ có thư viện React Native Animated, chúng ta có thể lấy được các animation jank-free và cả các interaction-driven animation như scrolling parallax.

JS/React Open Source

Vì React Native chạy React và JavaScript, chúng tôi đã tận dụng rất nhiều array các project javascript như redux, reselect, jest, v.v

Flexbox

React Native xử lý layout bằng Yoga – một library C đa platform giúp xử lý các tính toán layout qua flexbox API. Trước đó, chúng tôi vướng phải cá nhiều giới hạn với Yoga, ví dụ như, thiếu aspect ratios nhưng sau đó nó đã được bổ sung trong các update sau này. Ngoài ra, các tutorial như flexbox froggy làm cho việc onboarding thêm thú vị.

Kết hợp với Web

Lâu sau khi đã ứng dụng React Native, chúng tôi bắt đầu build web, iOS, và Android cũng một lúc. Vì web cũng dùng Redux, chúng tôi phát hiện ra nhiều code có thể share xuyên web và các native platform mà không cần thay đổi gì.

Những mặt không hiệu quả

Sự non nớt của React Native

React Native mới hơn Android hoặc iOS nhiều. Nó mới hơn, tham vọng hơn, di chuyển nhanh chóng hơn. Mặc dù React Native hầu như hiệu quả mọi lúc, có rất nhiều ví dụ về sự độ non của nó và đôi khi sẽ mang lại những thứ rắc rối không đáng. Đáng tiếc là, những thành phần này rất khó xác định và sẽ tốn của bạn từ vài tiếng đến vài ngày để giải quyết được nó.

Maintain fork của React Native

Do độ trẻ của React Native mà đôi khi chúng ta phải patch React Native source. Ngoài việc quay trở lại React Native, chúng tôi cũng phải maintain fork để chúng ta có thể nhanh chóng gộp các thay đổi vào phiên bản mình đang sử dụng. Qua 2 năm, chúng tôi đã phải add gần 50 commit trên React Native, làm cho quá trình upgrade React Native trở thành một trải nghiệm rất đau đớn.

JavaScript Tooling

JavaScript là một ngôn ngữ untyped. Nó làm cho việc scale khó khăn hơn và sẽ là một điểm trừ quá lớn với những kĩ sư mobile nào đang muốn học React Native. Chúng tôi phát hiện ra rằng việc áp dụng flow trừ các cryptic error message sẽ đem đến hậu quả khôn lường. Chúng tôi cũng đã cho ra TypeScript nhưng gộp nó vào một infrastructure có sẵn như babel và metro bundler sẽ rất phiền phức. Tuy nhiên, chúng tôi vẫn sẽ tiếp tục nghiên cứu nhiều hơn về TypeScript trên web.

Refactoring

Vì là một ngôn ngữ untype nên việc refactor trên JavaScript là cực khó và nhiều lỗi. Một cơn ác mộng để refactoring chính xác đó là Đặt lại tên cho prop, đặc biệt là các tên prop phổ biến như onClick hoặc các prop được pass qua nhiều component. Tệ hơn là, các refactor sẽ hỏng trong production chứ không phải lúc compile và rất khó để add phần phân tích hợp lý vào.

Các JavaScriptCore inconsistency

Một khía cạnh khó nói khác của React Native đó là nó chạy trong môi trường JavaScriptCore. Đây là một số kết quả mà chúng tôi nhận được khi thử:

  • iOS ship chung với JavaScriptCore của riêng nó, đồng nghĩa là iOS khá thống nhất và sẽ không gây nhiều khó khăn cho chúng tôi.
  • Android không ship JavaScriptCore nên React Native sẽ tự bundle cho nó. Tuy nhiên, cái bạn lấy được mặc định là cái cũ. Do đó, chúng ta cũng phải tự tìm ra cách để lấy được cái mới hơn.
  • Khi debug, React Nativese sẽ attach vào Chrome Developer Tool. Điều này là tốt vì nó là một debugger rất quyền lực. Tuy nhiên, một khi debugger đã attach, mọi JavaScript sẽ chạy trong V8 engine của Chrome. Nó sẽ chạy tốt 99.9% thời gian. Tuy nhiên, giả dụ như, bạn gặp phải vấn đề khi toLocaleString chạy trên iOS nhưng lúc debug thì chỉ chạy trên Android. Thành ra Android JSC sẽ không có nó và âm thầm fail trừ khi bạn đang debug khi đang sử dụng V8. Nếu như không biết nhưng chi tiết kĩ thuật như thế này, nó sẽ trở thành một trải nghiệm debug đầy đau thương cho các kĩ sư product.

Các React Native Open Source Library

Học một platform mới không hề dễ dàng và rất tốn thời gian. Hầu hết mọi người chỉ rành rõi một đến hai platform. Các React Native library có các native bridge như map, video, v.v đòi hỏi một lượng kiến thức trên cả 3 platform để hoạt động hiệu quả. Nó dẫn đến inconsistency hoặc các bug ngẫu nhiên trên Android hoặc iOS.

Trên Android, nhiều React Native library cũng yêu cầu bạn phải có chuyển môn đủ sâu về node_modules hơn là publish các maven artifact không khớp với cái mà cộng đồng mong đợi.

Infrastructure song song và Feature Work

Chúng tôi đã tích luỹ được nhiều năm với native infrastructure trên Android và iOS. Tuy nhiên, với React Native, chúng tôi bắt đầu từ 2 bàn tay trắng và phải viết và tạo lại hết các bridge trên tất cả những infrastructure sẵn có. Nó có nghĩa là có lúc kĩ sư của chúng tôi cần đến những tính năng chưa hề tồn tại nữa! Lúc này họ phải làm việc trên platform mà mình không hiểu gì và ngoài phạm vi project hoặc tạm hoãn cho đến khi tính năng xuất hiện…

Crash Monitoring

Chúng tôi dùng Bugsnag để report crash trên Android và iOS. Mặc dù chúng tôi có thể làm cho Bugsnag hoạt động trên cả 2 platform, nó không đáng tin lắm và đòi hỏi nhiều hơn so với các platform khác. Vì React Native còn mới và hiếm trong ngành, chúng tôi phải build một lượng infrastructure khá lớn như upload source của map in-house và phải dạy Bugsnag làm những thứ như filter crash bằng những gì đang có trong React Native.

Do lượng custom infrastructure trong React Native, chúng ta thường vướng phải những vấn đề khá nghiêm trọng mà không được report về crash hoặc source map không được upload hợp lý.

Cuối cùng, debug các React Native crash thường khó khăn hơn nhiều nếu như vấn đề này dính cả React Native và native code vì stack trace không nhảy qua lại giữa React Native và native được.

Native Bridge

React Native có một bridge API để giao tiếp giữa native và React Native. Dù nó hoạt động đúng như mong đợi, việc viết là một thử thách. Đầu tiên, nó đòi hỏi cả 2 môi trường phải được set up đúng. Chúng tôi cũng vướng phải một số vấn đề liên quan đến các type bất thường từ JavaScript. Ví dụ, các integer thường được wrap bởi string, một vấn đề ít được phát hiện ra cho đến khi nó được pass qua bridge. Tệ hơn là, đôi khi iOS sẽ âm thâm fail khi Android crash. Chúng tôi từ đó cũng bắt đầu nghiên cứu cách để tạo bridge code tự động từ các định nghĩa của TypeScript đến cuối năm 2017, hơi quá muộn.

Thời gian Initialization

Không như các native screen, việc render React Native đòi hỏi ít nhất một full main thread -> js -> yoga layout thread -> main thread trước khi có đủ info để render một screen lần đầu. Chúng ta đã gặp một p90 render trung bình 280ms trên iOS và 440ms trên Android. Trên Android, chúng ta đã dùng postponeEnterTransition API thường dùng cho shared element transition để trì hoãn việc hiển thị screen cho đến khi nó được render. Trên iOS, sẽ vướng phải các vấn đề về cài navbar configuration từ React Native. Từ đó, chúng ta sẽ add các khoảng trì hoãn cỡ 50ms với mọi React Native screen transition để hạn chế việc navbar chập chờn sau khi đã load configuration.

App Size

React Native cũng có những tác động không nhỏ đến app size. Trên Android, total size của React Native (Java + JS + các native library như Yoga + Javascript Runtime) là 8mb/ ABI. Với cả x86 và arm (chỉ 32 bit) trong một APK, nó có thể xấp xỉ 12mb.

64-bit

Chúng ta vẫn chưa thể ship một 64-bit APK trên Android vì vấn đề này.

Các Gesture

Chúng tôi tránh dùng React Native với screen bao gồm các gesture phức tạp vì touch subsystem trên Android và iOS đã đủ khó để nảy sinh thêm một API thống nhất gây khó dễ cho cả cộng đồng React Native. Tuy nhiên, mọi thức vẫn phải tiến triển và react-native-gesture-handler vừa mới đạt bản 1.0.

Các list dài 

React Native đã cải thiện được vấn đề với các library như FlatList. Tuy nhiên, nó vẫn chưa đủ lớn và linh hoạt như RecyclerView trên Android hoặc UICollectionView trên iOS. Rất nhiều hạn chế rất khó vượt qua do threading. Adapter data không thể truy cập đồng bộ được nên bạn có thể thấy thấy các flash khi đang lướt vì nó render không đồng bộ khi lướt nhanh. Text cũng không được đo đồng bộ nên iOS không thể tối ưu hoá một số cái bằng pre-computed cell heights.

Upgrade React Native

Dù hầu hết các upgrade React Native đều không đáng kể, vẫn sẽ có vài cái đau điếng như cục nhọt. Cụ thể là, việc chuyển React Native 0.43 (tháng 04/2017) sang 0.49 (tháng 10/2017) là không thể vì nó dùng React 16 alpha và beta. Đây là một vấn đề lớn vì hầu hết các library React được thiết kế cho web không support các phiên bản React trước đó. Quy trình vật lộn với các dependency cho upgrade này là một thiệt hại lớn cho React Native infrastructure work vào giữa năm 2017.

Tính truy cập

Trong năm 2017, chúng ta đã thực hiện một thay đổi lớn về accessibility – chúng tôi nỗ lực phát triển sao cho cả người khuyết tật cũng có thể dùng Airbnb để book cái họ muốn. Tuy nhiên, có quá nhiều lỗ hổng trong các API React Native accessibility. Để làm nên một thanh accessibility bar ở mức tối thiểu, chúng tôi phải maintain fork của chính mình trên React Native để gộp các chỉnh sửa lại. Trong những trường hợp này, những chỉnh sửa trong 1 dòng code Android hoặc iOS cũng tốn đến vài ngày để tìm hiểu xem add nó vào React Native thế nào, chọn lọc cẩn thận, rồi file vấn đề trên React Native core và follow nó vài tuần sau đó.

Các Crash phiền phức

Đôi khi chúng tôi phải đối mặt với rất nhiều dạng crash kì lạ và khó fix. Ví dụ, chúng ta đang phải fix crash này trên @ReactProp annotation và không thể tái tạo nó trên bất kì device nào, kể cả trên các device cùng hardware và software đến những cái crash khác.

SavedInstanceState trong các Process trên Android

Android thường dọn sạch các background process nhưng sau đó sẽ cho chúng lưu phần state đồng bộ trong bundle. Tuy nhiên, trên React Native, mọi state chỉ truy cập được trên js thread nên không thể thực hiện nó đồng bộ được. Kể cả như không trong trường hợp đó, redux như một state store sẽ không tương thích với phương pháp này vì nó chứa data cả chuyển đổi được và không chuyển đổi được và có thể chứa nhiều data hơn khả năng trong savedInstanceState bundle mà có thể dẫn đến crash trong production.


Đây là phần 2 trong series React Native của chúng tôi. 3 more to go!

  React Native tại Airbnb (P1): Mở đầu
  Đi phỏng vấn vị trí React Native 6 tháng kinh nghiệm cần những gì ?

TopDev via Medium