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

Hoàng Vĩnh

Các chi tiết kỹ thuật

React Native bản thân là một nền tảng tương đối mới và phát triển nhanh so với mặt cắt ngang của Android, iOS, web và các framework web khác. Sau hai năm, chúng ta có thể nói một cách an toàn rằng React Native là một cuộc cách mạng theo nhiều cách. Đó là một sự thay đổi mô hình cho điện thoại di động và chúng ta đã có thể gặt hái nhiều lợi ích từ những mục tiêu của nó. Tuy nhiên, lợi ích của nó không đến mà không có những điểm thiệt thòi đáng kể.

Những chi tiết hiệu quả

Đa nền tảng

Lợi ích chính của React Native thực chất là mã bạn viết, chạy nguyên bản trên Android và iOS. Hầu hết các tính năng sử dụng React Native đều có thể đạt được 95-100% mã chung và 0,2% tệp là nền tảng chuyên biệt (* .android.js / *. Ios.js).

Hệ thống ngôn ngữ thiết kế hợp nhất (Unified Design Language System)

Chúng tôi đã phát triển một ngôn ngữ thiết kế đa nền tảng được gọi là Unified Design Language System (DLS). Có các phiên bản Android, iOS, React Native và web của mọi thành phần. Có một ngôn ngữ thiết kế hợp nhất là có thể dễ dàng viết các tính năng đa nền tảng, vì có nghĩa là thiết kế, tên component và màn hình đều nhất quán trên các nền tảng. Tuy nhiên, chúng tôi vẫn có thể đưa ra quyết định phù hợp với từng nền tảng nếu có. Ví dụ: chúng tôi sử dụng Toolbar native trên Android và UINavigationBar trên iOS và chọn ẩn các chỉ báo tiết lộ trên Android vì chúng không tuân thủ nguyên tắc thiết kế nền tảng Android.

Chúng tôi đã chọn viết lại các component thay vì sử dụng các component native vì sẽ đáng tin cậy hơn để tạo từng API phù hợp cho từng nền tảng và giảm chi phí bảo trì cho các kỹ sư Android và iOS, những người có thể không biết cách kiểm tra các thay đổi trong React Native. Tuy nhiên, điều này đã gây ra sự phân mảnh giữa các nền tảng trong đó các phiên bản gốc và React Native của cùng một thành phần sẽ không đồng bộ.

React

Có một lý do mà React là web framework được yêu thích nhất. Nó đơn giản nhưng mạnh mẽ và khả năng mở rộng quy mô tốt. Một số điều chúng tôi đặc biệt thích là:

  • Component: React Components được sử dụng tốt với props và state. Đây là một đóng góp chính cho khả năng mở rộng của React.
  • Vòng đời đơn giản hóa: lifecycle của Android và iOS nổi tiếng là phức tạp. Các components của React cơ bản giải quyết vấn đề này và làm cho việc học React Native đơn giản hơn đáng kể so với việc học Android hoặc iOS.
  • Khai báo: Bản chất khai báo của React đã giúp cho UI đồng bộ với trạng thái cơ bản.

Tốc độ lặp lại

Khi phát triển với React Native, chúng tôi có thể sử dụng hot reloading để thử nghiệm các thay đổi trên Android và iOS chỉ một hoặc hai giây. Mặc dù hiệu suất build là ưu tiên hàng đầu cho các ứng dụng native của chúng tôi, thì vẫn không thể bằng với tốc độ lặp lại mà chúng tôi đã đạt được với React Native. Vào các giai đoạn đỉnh điểm, thời gian biên dịch với native là 15 giây nhưng có thể cao tới 20 phút đối với các bản build đầy đủ.

Đầu tư vào cơ sở hạ tầng

Chúng tôi đã phát triển tích hợp rộng rãi vào cơ sở hạ tầng native của mình. Tất cả các phần cốt lõi như kết nối mạng, i18n, thử nghiệm, chuyển đổi phần tử chung, thông tin thiết bị, thông tin tài khoản và nhiều phần khác được gói lại trong một API React Native duy nhất. Những sự kết nối này là một trong số phần phức tạp hơn do chúng tôi muốn gói các API Android và iOS hiện tại vào một cái gì đó nhất quán và chuẩn mực cho React. Trong khi giữ cho những cây cầu này được cập nhật với sự lặp lại nhanh chóng và sự phát triển cơ sở hạ tầng mới là một trò chơi cần phải liên tục bắt kịp, đầu tư của nhóm cơ sở hạ tầng đã làm cho sản phẩm hoạt động dễ dàng hơn nhiều.

Nếu không có sự đầu tư khổng lồ này vào cơ sở hạ tầng, React Native sẽ dẫn đến trải nghiệm kém. Kết quả là, chúng tôi không tin rằng React Native có thể đơn giản được đưa vào ứng dụng hiện có mà không cần đầu tư đáng kể và liên tục.

Hiệu suất

Một trong những mối quan tâm lớn nhất về React Native là hiệu suất của nó. Tuy nhiên, trong thực tế, điều này hiếm khi là một vấn đề. Hiệu suất thường được nghĩ theo một chiều. Chúng tôi thường xuyên thấy các kỹ sư di động xem xét JS và nghĩ “chậm hơn Java”. Tuy nhiên, việc di chuyển các business logic và layout ra khỏi main thread thực sự cải thiện hiệu suất render trong nhiều trường hợp.

Khi chúng tôi đã nhận ra các vấn đề về hiệu suất, chúng thường được gây ra bởi việc render quá mức và được giảm nhẹ bằng cách sử dụng hiệu quả các tệp shouldComponentUpdate, removeClippedSubviews và sử dụng Redux tốt hơn.

Tuy nhiên, thời gian khởi tạo và lần render đầu tiên (được nêu dưới đây) đã khiến React Native hoạt động kém cho màn hình khởi chạy, liên kết sâu và tăng thời gian TTI trong khi điều hướng giữa các màn hình. Ngoài ra, các màn hình bị tụt khung hình khó để gỡ lỗi vì Yoga biên dịch giữa các thành phần React Native và các native views.

Redux

Chúng tôi đã sử dụng Redux cho quản lý trạng thái mà chúng tôi thấy hiệu quả và ngăn giao diện người dùng khỏi việc không đồng bộ với trạng thái và cho phép chia sẻ dữ liệu dễ dàng trên các màn hình. Tuy nhiên, Redux mang tiếng là khá khó để học. Chúng tôi cung cấp các bộ cung cấp cho một số mẫu phổ biến nhưng nó vẫn là một trong những phần khó khăn nhất và nguồn gây nhầm lẫn khi làm việc với React Native. Cần lưu ý rằng những thách thức này không riêng chỉ React Native.

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

Bởi vì mọi thứ trong React Native có thể được kết nối bằng mã native, chúng tôi cuối cùng có thể xây dựng được nhiều thứ mà lúc đầu nghĩ là bất khả thi, như là:

  • Chuyển đổi phần tử chung: Chúng tôi đã tạo thành phần <SharedElement> được hỗ trợ bởi mã phần tử chung gốc trên Android và iOS. Điều này thậm chí hoạt động tốt giữa màn hình Native và React Native.
  • Lottie: Chúng tôi đã có thể khiến Lottie làm việc trong React Native bằng cách gói các thư viện hiện có trên Android và iOS.
  • Ngăn xếp mạng native: React Native sử dụng ngăn xếp mạng cục bộ và bộ nhớ cache hiện có của chúng tôi trên cả hai nền tảng.
  • Hạ tầng cốt lõi khác: Cũng giống như mạng lưới, chúng tôi gói phần còn lại của cơ sở hạ tầng gốc hiện có như i18n, thử nghiệm, v.v. để nó hoạt động liên tục trong React Native.

Phân tích tĩnh

Chúng tôi có một lịch sử mạnh về việc sử dụng eslint trên web mà có khả năng tận dụng. Tuy nhiên, chúng tôi là nền tảng đầu tiên tại Airbnb khám phá ra prettier. Nhận ra rằng nó có hiệu quả trong việc giảm nits và bikeshedding trên PRs. Prettier hiện đang được điều tra tích cực bởi nhóm cơ sở hạ tầng web của chúng tôi.

Chúng tôi cũng sử dụng phân tích để đo lường thời gian kết xuất và hiệu suất để tìm ra màn hình nào là ưu tiên hàng đầu để điều tra các vấn đề về hiệu suất.

Bởi vì React Native nhỏ hơn và mới hơn so với cơ sở hạ tầng web của chúng tôi, nó được chứng minh là một nơi tốt để thử nghiệm những ý tưởng mới. Nhiều công cụ và ý tưởng mà chúng tôi đã tạo cho React Native hiện đang được áp dụng bởi các trang web.

Ảnh động

Nhờ thư viện React Native Animated, chúng tôi đã có thể thực hiện được các hoạt ảnh không bị giật và cả các hình động định hướng tương tác như di chuyển thị sai.

Mã nguồn mở JS/React

Bởi vì React Native chạy React và javascript, chúng tôi đã có thể tận dụng các mảng cực lớn của các dự án javascript như redux, reselect, jest, v.v.

Flexbox

React Native xử lý bố cục với Yoga, một thư viện C đa nền tảng xử lý các tính toán bố cục thông qua API flexbox. Ban đầu, chúng tôi bị ảnh hưởng bởi những hạn chế của Yoga như thiếu tỷ lệ khung hình nhưng sau đó đã được thêm vào các bản cập nhật tiếp theo. Thêm vào đó, các hướng dẫn thú vị như fobggy flexbox giúp cho việc sử dụng trở nên thú vị hơn.

Cộng tác với Web

Trong cuộc khám phá React Native gần đây, chúng tôi đã bắt đầu xây dựng cho web, iOS và Android cùng một lúc. Vì web cũng sử dụng Redux, chúng tôi đã tìm thấy nhiều mã lệnh có thể được chia sẻ trên nền tảng web và nền tảng gốc mà không bị thay đổi.

Những điều còn chưa hiệu quả

React Native chưa trưởng thành

React Native kém trưởng thành hơn Android hoặc iOS. Nó mới hơn, có tham vọng cao và phát triển với tốc độ cực nhanh. Trong khi React Native hoạt động tốt trong hầu hết các tình huống, có những trường hợp mà trong đó sự non nớt của nó được thấy rõ. Thật không may, những trường hợp này khó để dự đoán và có thể mất hàng giờ thậm chí vài ngày để xử lý.

Duy trì một Fork của React Native

Do tính không trưởng thành của React Native, đã có những lúc chúng ta cần phải vá nguồn React Native. Ngoài việc đóng góp lại cho React Native, chúng tôi đã phải duy trì một fork nơi có thể nhanh chóng hợp nhất các thay đổi và chạy phiên bản của chúng tôi. Trong hai năm, chúng tôi đã phải thêm khoảng 50 commit cho sự phát triển của React Native. Điều này làm cho quá trình nâng cấp React Native cực kỳ khó khăn.

Công cụ JavaScript

JavaScript là một ngôn ngữ không được phân loại. Sự thiếu type safety gây khó khăn cho việc mở rộng quy mô và trở thành điểm tranh cãi cho các kỹ sư di động dùng các ngôn ngữ được phân loại, những người mà, tuy nhiên, lại cảm thấy thích thú học hỏi React Native. Chúng tôi đã khám phá việc áp dụng luồng nhưng những tin nhắn bị mã hóa lỗi đã dẫn đến trải nghiệm gây khó chịu. Chúng tôi cũng đã khám phá TypeScript nhưng tích hợp nó vào cơ sở hạ tầng hiện có của như gói babel và metro được chứng minh là có vấn đề. Tuy nhiên, chúng tôi đang tiếp tục chủ động điều tra việc sử dụng TypeScript trên web.

Refactoring

Một hiệu ứng phụ của JavaScript không được giải mã là việc refactoring mã vô cùng khó khăn và dễ bị lỗi. Đổi tên props, đặc biệt là props với một tên phổ biến như onClick hoặc props được truyền qua nhiều component thực sự là một cơn ác mộng để tái cấu trúc một cách chính xác. Để làm cho vấn đề tồi tệ hơn, các nhà tái cấu trúc đã phá trong quá trình sản xuất thay vì tại thời gian biên dịch và rất khó để thêm vào phân tích tĩnh thích hợp.

JavaScriptCore nhất quán

Một khía cạnh tinh vi và phức tạp của React Native là một phần là do nó được thực hiện trong môi trường JavaScriptCore. Dưới đây là những hệ quả mà chúng tôi gặp phải:

  • iOS có JavaScriptCore riêng. Điều này có nghĩa là iOS hầu hết nhất quán và không có vấn đề.
  • Android không gửi JavaScriptCore của riêng nó để React Native tự gói. Tuy nhiên, cái bạn nhận được theo mặc định thường lỗi mốt. Kết quả là, chúng tôi tự mình gói một cái mới hơn.
  • Trong khi debug, React Native gắn liền với Chrome Developer Tools. Điều này là rất tốt vì nó là một trình debug mạnh mẽ. Tuy nhiên, một khi debugger được đính kèm, tất cả JavaScript đều chạy trong engine V8 của Chrome. 99.9% mọi thứ sẽ ổn. Tuy nhiên, giả dụ, chúng tôi lấy được bit khi toLocaleString làm việc trên iOS nhưng chỉ có thể làm việc trên Android khi đang debug. Hóa ra Android JSC không bao gồm nó và hoàn toàn là một sai lầm trừ phi bạn đang debug trong trường hợp sử dụng engine V8. Nếu không biết các chi tiết kỹ thuật này, các kỹ sư sản phẩm sẽ cực kỳ khó để debug.

Thư viện nguồn mở React Native

Học một nền tảng rất khó và tốn thời gian. Hầu hết mọi người chỉ biết rõ một hoặc hai nền tảng. Các thư viện gốc React Native có các kết nối đến các thư viện native như bản đồ, video, v.v. và đòi hỏi kiến thức tương đương về cả ba nền tảng để thành công. Nhận thấy rằng hầu hết các dự án nguồn mở của React Native đều được viết bởi những người chỉ có kinh nghiệm cơ bản. Điều này dẫn đến sự không nhất quán hoặc lỗi không mong muốn trên Android hoặc iOS.

Trên Android, nhiều thư viện React Native cũng yêu cầu bạn sử dụng đường dẫn tương đối đến node_modules thay vì xuất bản các tạo phẩm maven không nhất quán với những gì cộng đồng mong đợi.

Cơ sở hạ tầng song song và tính năng làm việc

Chúng tôi đã tích lũy trong nhiều năm cơ sở hạ tầng gốc trên Android và iOS. Tuy nhiên, trong React Native, chúng tôi bắt đầu với một tờ giấy trắng và đã phải viết hoặc tạo ra các kết nối cho tất cả các cơ sở hạ tầng hiện có. Điều này có nghĩa là có những lúc mà một kỹ sư sản phẩm cần một số chức năng chưa tồn tại. Tại thời điểm đó, họ phải làm việc trong một nền tảng mà họ không quen thuộc và nằm ngoài phạm vi dự án của họ để xây dựng nó hoặc bị chặn cho đến khi nó có thể được tạo ra.

Giám sát sự cố

Chúng tôi sử dụng Bugsnag để báo cáo sự cố trên Android và iOS. Mặc dù có thể làm cho Bugsnag hoạt động trên cả hai nền tảng nhưng sẽ ít đáng tin cậy hơn và yêu cầu nhiều công việc hơn so với các nền tảng khác. Bởi vì React Native khá mới và hiếm trong ngành, chúng tôi phải xây dựng một số lượng đáng kể cơ sở hạ tầng như tải bản đồ nguồn trong nhà và phải làm việc với Bugsnag để có thể thực hiện những việc như lỗi bộ lọc trong React Native.

Do số lượng cơ sở hạ tầng tùy chỉnh xung quanh React Native, đôi khi chúng tôi gặp sự cố nghiêm trọng trong đó các sự cố không được báo cáo hoặc nguồn không được tải lên đúng cách.

Cuối cùng, việc debug các sự cố trong React Native thường gặp nhiều khó khăn hơn nếu sự cố mở rộng trong React Native và mã native do các stack không di chuyển giữa React Native và native.

Cầu nối với Native

React Native có API cầu nối để giao tiếp giữa Native và React Native. Dù hoạt động như mong đợi, nhưng nó cực kỳ cồng kềnh để viết. Thứ nhất, nó đòi hỏi tất cả ba môi trường phát triển phải được thiết lập đúng. Chúng tôi cũng gặp nhiều vấn đề trong đó các loại đến từ JavaScript rất khó ngờ. Ví dụ, các số nguyên thường được gói bởi các chuỗi, vấn đề không được nhận ra cho đến khi nó được kết nối. Chúng tôi bắt đầu điều tra tự động tạo mã cầu nối từ các định nghĩa TypeScript vào cuối năm 2017 nhưng đã quá muộn.

Thời gian khởi tạo

Trước khi React Native có thể kết xuất lần đầu tiên, bạn phải khởi tạo thời gian chạy. Không may, phải mất vài giây cho một ứng dụng có kích thước như ý muốn, ngay cả trên một thiết bị cao cấp. Điều này khiến việc sử dụng React Native cho màn hình khởi chạy gần như bất khả thi. Chúng tôi đã giảm thiểu thời gian kết xuất đầu tiên cho React Native bằng cách khởi chạy nó tại tính năng ứng dụng.

Thời gian render

Không giống như màn hình trong native, việc kết xuất React Native yêu cầu ít nhất một main thread đầy đủ -> js -> layout thread yoga -> main thread trước khi có đủ thông tin để hiển thị màn hình lần đầu tiên. Chúng tôi đã thấy kết quả hiển thị p90 ban đầu trung bình là 280ms trên iOS và 440ms trên Android. Trên Android, chúng tôi đã sử dụng postponeEnterTransition API thường được sử dụng cho các chuyển đổi phần tử chung để trì hoãn hiển thị màn hình cho đến khi nó kết xuất xong. Trên iOS, chúng tôi đã gặp sự cố khi đặt cấu hình thanh điều hướng từ React Native. Kết quả là, chúng tôi thêm độ trễ nhân tạo là 50ms cho tất cả các lần chuyển đổi màn hình Native React để ngăn thanh điều hướng nhấp nháy khi cấu hình được tải.

Kích thước ứng dụng

React Native cũng có tác động không đáng kể đến kích thước ứng dụng. Trên Android, tổng kích thước của React Native (Java + JS + các thư viện gốc như Yoga + Javascript Runtime) là 8mb trên mỗi ABI. Với cả x86 và arm (chỉ 32 bit) trong một APK, nó sẽ gần hơn đến 12mb.

64 bit

Chúng tôi vẫn chưa thể làm APK 64 bit trên Android.

Các Gestures

Chúng tôi đã tránh sử dụng React Native cho các màn hình có liên quan đến gestures phức tạp vì hệ thống điều khiển cho Android và iOS khác nhau khi đưa ra một API hợp nhất đã trở thành thách thức cho toàn bộ cộng đồng React Native. Tuy nhiên, công việc đang tiếp tục tiến triển và react-native-gesture-handler chỉ mới đạt 1.0.

Lists

React Native đã thực hiện một số tiến bộ trong lĩnh vực này với các thư viện như FlatList. Tuy nhiên, chúng không ở đâu gần với trạng thái trưởng thành và linh hoạt của RecyclerView như ở Android hoặc UICollectionView trên iOS. Nhiều hạn chế rất khó khắc phục do luồng. Không thể truy cập đồng bộ dữ liệu bộ điều hợp để có thể xem các chế độ xem flash vì chúng được hiển thị không đồng bộ trong khi cuộn nhanh. Văn bản cũng không được đo đồng bộ dẫn đến iOS không thể thực hiện tối ưu hóa nhất định với chiều cao ô được tính trước.

Nâng cấp React Native

Mặc dù hầu hết các bản nâng cấp React Native đều không đáng kể, nhưng vẫn có một số điều cực kỳ khó khăn. Đặc biệt, gần như không thể sử dụng React Native 0.43 (tháng 4 năm 2017) đến 0.49 (tháng 10 năm 2017) vì nó sử dụng React 16 alpha và beta. Điều này gây khó khăn vì hầu hết các thư viện React được thiết kế để sử dụng web không hỗ trợ các phiên bản React pre-release. Quá trình tranh luận các phụ thuộc thích hợp cho việc nâng cấp này là một thiệt hại lớn cho công việc cơ sở hạ tầng Native React khác vào giữa năm 2017.

Trợ năng

Vào năm 2017, chúng tôi đã thực hiện một cuộc đại tu khả năng tiếp cận lớn trong đó đầu tư những nỗ lực đáng kể để đảm bảo rằng người khuyết tật có thể sử dụng Airbnb để đặt một danh sách có thể đáp ứng nhu cầu của họ. Tuy nhiên, đã có nhiều lỗ hổng trong khả năng truy cập React Native của API. Để đạt được thanh trợ năng tối thiểu, chúng tôi đã phải duy trì phiên bản fork React Native riêng của chúng tôi, nơi chúng tôi có thể hợp nhất các bản sửa lỗi. Đối với những trường hợp này, sửa chữa một dòng trên Android hoặc iOS sẽ mất đến vài ngày để tìm hiểu cách thêm nó vào React Native, chọn lựa, sau đó gửi vấn đề lên lõi React Native và theo dõi nó trong vài tuần tiếp theo.

Lỗi Crashes

Chúng tôi đã phải đối phó với một vài lỗi crash rất lạ và khó sửa chữa. Ví dụ: chúng tôi hiện đang gặp phải sự cố này trên chú thích @ReactProp và không thể tạo lại nó trên bất kỳ thiết bị nào, ngay cả những thiết bị có phần cứng và phần mềm giống với những phần mềm đang gặp sự cố.

SavedInstanceState trên Android

Android thường xuyên dọn dẹp các quy trình nền nhưng lại giúp lưu đồng bộ trạng thái của chúng trong một nhóm. Tuy nhiên, trên React Native, tất cả trạng thái chỉ có thể truy cập trong chuỗi js vì vậy không thể thực hiện đồng bộ. Ngay cả khi không phải trường hợp đó, redux hoạt động như một kho lưu trữ trạng thái sẽ không tương thích với cách tiếp cận này do nó chứa hỗn hợp dữ liệu tuần tự và không tuần tự và có thể chứa nhiều dữ liệu hơn so với gói có thể được lưu trong gói savedInstanceState dấn đến lỗi crash trong quá trình.

Tác giả: Gabriel Peal | Nguồn: medium.com