+1

[React Native] Guide - Performance - Phần 1

Một lý do thuyết phục để sử dụng React Native thay vì các công cụ dựa trên WebView là để hiển thị được 60 khung hình mỗi giây và một giao diện tương tự native cho ứng dụng của bạn. Những nơi có thể, chúng tôi muốn React Native làm đúng như nền tảng cơ bản và giúp bạn tập trung vào ứng dụng của bạn thay vì tối ưu hóa hiệu suất, nhưng có những lĩnh vực mà chúng tôi chưa thực sự hoàn thiện và những nơi khác mà React Native (tương tự như viết native code) có thể không thể tìm được cách tốt nhất để tối ưu hóa cho bạn, vì thế đôi khi sự can thiệp bằng tay sẽ là cần thiết. Chúng tôi cố gắng hết sức để cung cấp hiệu suất UI mượt mà tương tự mặc định, nhưng đôi khi điều đó là không thể.

Hướng dẫn này với mục tiêu dạy cho bạn một vài điều căn bản để giúp bạn có thể phát hiện vấn đề hiệu suất, cũng như là thảo luận về source chung về các vấn đề và hướng giải quyết.

Những thứ bạn cần biết về frames

Nhưng movies trước đây được tạo ra được gọi là "moving pictures" vì một lý do: chuyển động thực tế trong video là ảo được tạo ra bằng cách thay đổi nhanh hình ảnh tĩnh với tốc độ nhất quán. Chúng tôi đề cập đến mỗi hình ảnh dưới dạng frames. Số frames được hiển thị mỗi giây ảnh hưởng trực tiếp đến mức độ trơn tru và tương tự như video (hoặc giao diện người dùng) có vẻ như vậy. Thiết bị iOS hiển thị 60 frames mỗi giây, trong khi và hệ thống UI cần khoảng 16.67ms để thực hiện tất cả công việc cần thiết để tạo ra hình ảnh tĩnh (frame) mà người dùng sẽ thấy trên màn hình trong khoảng thời gian đó. Nếu bạn không thể làm các công việc cần thiết để tạo ra khung đó trong 16.67ms được phân bổ thì bạn sẽ "mất một frame" và trong giao diện người dùng nó sẽ không xuất hiện.

Bây giờ sẽ có một chút khó hiểu, hãy mở developer menu trong ứng dụng của bạn và bật chương trình Show Perf Monitor. Bạn sẽ nhận thấy rằng có hai tỷ lệ khung hình khác nhau.

Show_Perf_Monitor

JS frame rate (JavaScript thread)

Đối với hầu hết các ứng dụng React Native, business logic của bạn sẽ chạy trên JavaScript. Đây là nơi ứng dụng React của bạn đang hoạt động, các cuộc gọi API được thực hiện, xử lý các sự kiện chạm trên màn hình, v.v ... Cập nhật native-backed views được chuyển thể và gửi tới các điểu khiển native sau đó kết thúc với các vòng lặp của sự kiện đó, trước mỗi thời gian hiển thị của frame (nếu như tất cả đều hiển thị tốt). Nếu JavaScript không phản hồi cho một frames, nó sẽ được coi là một frame bỏ. Ví dụ, nếu bạn gọi this.setState trên thành phần gốc của một ứng dụng phức tạp và kết quả trả lại là render lại các thành phần cũ, thì có thể hiểu rằng điều này có thể mất 200ms và kết quả là 12 frames bị bỏ. Bất kỳ hoạt ảnh nào được kiểm soát bởi JavaScript dường như đóng băng trong thời gian đó. Nếu có gì dài hơn 100ms, người dùng sẽ cảm thấy nó.

Điều này thường xảy ra trong quá trình chuyển đổi của Navigator: khi bạn đẩy một route mới, luồn JavaScript cần hiển thị tất cả các thành phần cần thiết cho cảnh để gửi qua các lệnh thích hợp tới native để tạo lại view. Thông thường để công việc được hoàn tất thì nó sẽ lấy mất một vài frames và căn nguyên của jank là bởi vì việc chuyển đổi đó được điểu khiển bởi luồn của JavaScript. Đôi khi các thành phần sẽ làm thêm công việc trên componentDidMount, điều này có thể dẫn đến sự thay đổi thứ hai trong quá trình chuyển đổi.

Một ví dụ khác đó là phản hồi của hành động chạm: nếu bạn đang làm việc trên nhiều khung trong luồn của JavaScript, bạn có thể nhận thấy sự chậm trễ trong việc phản hồi với TouchableOpacity. Điều này là do luồng JavaScript đang bận và không thể xử lý sự kiện cảm ứng được gửi qua từ native. Kết quả là, TouchableOpacity không thể phản ứng với các sự kiện chạm và thể hiện để điều chỉnh độ mờ của nó.

UI frame rate (main thread)

Nhiều người đã nhận thấy rằng hiệu suất của NavigatorIOS là tốt hơn so với Navigator. Lý do cho điều này là các hình ảnh động cho quá trình chuyển đổi được thực hiện hoàn toàn trên main thread, và do đó chúng không bị gián đoạn bởi các khung bị mất trên luồng JavaScript.

Tương tự như vậy, bạn có thoải mái scroll thông qua một ScrollView khi luồng JavaScript bị khóa vì ScrollView đang thực thi trên main thread. Các sự kiện scroll được gửi đến luồng JS, nhưng không nhận được sự phản hồi để sự kiện scroll được thực thi.

Common sources of performance problems

Chạy ứng dụng trong chế độ development (dev=true)

Hiệu năng của luồng JavaScript bị ảnh hưởng rất nhiều khi chạy trong chế độ dev. Điều này là không thể tránh khỏi: có rất nhiều công việc phải thực hiện trong thời gian chạy để cung cấp cho bạn các cảnh báo và thông báo lỗi tốt, chẳng hạn như xác nhận hợp lệ propTypes và các giá trị khác. Cần luôn luôn chắc chắn kiểm tra hiệu suất trong build relesase.

Sử dụng lệnh console.log

When running a bundled app, these statements can cause a big bottleneck in the JavaScript thread. This includes calls from debugging libraries such as redux-logger, so make sure to remove them before bundling. Bạn cũng có thể sử dụng babel plugin loại bỏ tất cả việc gọi console.* . Trước tiên, bạn cần cài đặt nó với npm i babel-plugin-transform-remove-console --save, và sau đó chỉnh sửa tập tin .babelrc dưới thư mục dự án của bạn như sau:

{
  "env": {
    "production": {
      "plugins": ["transform-remove-console"]
    }
  }
}

Điều này sẽ tự động xóa tất cả các yêu cầu console.* trong bản release(production).

ListView khởi tạo và hiển thị quá chậm hoặc hiệu suất scroll không tốt với một list có số lượng lớn

Sử dụng thành phần mới như FlatList hoặc SectionList để thay thế. Bên cạnh việc đơn giản hoá API, các thành phần danh sách mới cũng có các cải tiến hiệu suất đáng kể, cái chính là nó sẽ chiếm dụng một vùng bộ nhớ cố định không đổi dù số lượng của các row có là bao nhiêu đi chăng nữa.

Nếu FlatList hiển thị chậm, hãy chắc chắn rằng bạn đã implement getItemLayout để tối ưu hóa tốc độ hiển thị bằng cách bỏ qua các phép đo kích thước của các item được hiển thị.

JS FPS nhúng vào khi vẽ lại một view mà view đó là khó thay đổi

Nếu bạn đang sử dụng ListView, bạn phải cung cấp một hàm rowHasChanged có thể làm giảm rất nhiều công việc bằng cách nhanh chóng xác định liệu một hàng cần phải được render lại hay không. Nếu bạn đang sử dụng cấu trúc dữ liệu không thay đổi, điều này sẽ đơn giản như kiểm tra tính tương đồng.

Tương tự, bạn có thể implement shouldComponentUpdate và chỉ ra các điều kiện chính xác các thành phần cẩn phải render lại. Nếu bạn viết các thành phần đơn giản (nơi mà giá trị trả lại của hàm render hoàn toàn phụ thuộc vào props và state), bạn có thể tận dụng PureRenderMixin để làm điều này cho bạn. Một lần nữa, các cấu trúc dữ liệu không thay đổi sẽ hữu ích để giữ cho tốc độ render được nhanh chóng - nếu bạn phải so sánh sâu về một danh sách lớn các đối tượng, có thể việc tái hiển thị toàn bộ thành phần của bạn sẽ nhanh hơn và chắc chắn sẽ yêu cầu ít mã hơn .

Loại bỏ FPS của luồng JS vì nó phải làm quá nhiều việc trong cùng thời gian chạy của luồng JavaScript

"Slow Navigator transitions" là sự biểu hiện phổ biến nhất của điều này, nhưng có những thứ khác khác mà điều này cũng có thể xảy ra. Sử dụng InteractionManager có thể là một cách tiếp cận tốt nhưng nếu yêu cầu trải nghiệm của người dùng quá cao để trì hoãn công việc trong một quá trình chạy animation, thì bạn nên muốn xem xét đến việc sử dụng LayoutAnimation.

Animated API hiện tại đang tính mỗi frames theo yêu cầu trên luồng JavaScript trừ khi bạn thiết lập useNativeDriver: true, trong khi LayoutAnimation cố gắng thực hiện Core Animation và không bị ảnh hưởng bởi luồng JS và main thread.

Một trong những trường hợp tôi đã sử dụng điều này là để chạy một animate trong một phương thức (sliding down từ trên xuống, và fade trong một lớp phủ mờ) trong khi khởi tạo đồng thời có thể nhận được phản hồi của một số request mạng, hiển thị nội dung của phương thức và cập nhật view nơi mà phương thức được thực hiện. Xem Animations để biết thêm thông tin về cách sử dụng LayoutAnimation.

Lưu ý:

LayoutAnimation chỉ hoạt động không dừng lại ("static" animations). Nếu bạn cần phải dừng đột ngột animation lại, bạn sẽ cần sử dụng Animated.

Di chuyển một view trên màn hình (scrolling, translating, rotating) bỏ luồng UI FPS

Điều này đặc biệt đúng khi bạn có văn bản với nền trong suốt nằm ở phía trên cùng của hình ảnh hoặc bất kỳ trường hợp nào khác nơi chuyển đổi giá trị alpha sẽ được yêu cầu vẽ lại view trên mỗi frame. Bạn sẽ tìm cách kích hoạt shouldRasterizeIOS hoặc renderToHardwareTextureAndroid có thể giúp bạn làm được việc này.

Hãy cẩn thận không lạm dụng nó hoặc sử dụng bộ nhớ của bạn. Thể hiện hiệu suất và mức sử dụng bộ nhớ khi sử dụng các props này. Nếu bạn không có kế hoạch để duy chuyển bất kỳ một view nào, bạn có thể tắt thuộc tính này đi.

Animating thay đổi kích thước của một hình ảnh giảm thiểu luồng UI FPS

Trên iOS, mỗi khi bạn điều chỉnh chiều rộng hoặc chiều cao của một thành phần Hình ảnh, nó sẽ được cắt lại và thu nhỏ lại từ hình ảnh ban đầu. Điều này có thể rất nặng, đặc biệt đối với hình ảnh lớn. Thay vào đó, sử dụng transform: [{scale}] để kích hoạt kích thước. Ví dụ khi bạn có thể làm điều này là khi bạn nhấn vào một hình ảnh và phóng to nó lên toàn màn hình.

TouchableX view không thường xuyên phản hồi

Đôi khi, nếu chúng ta thực hiện một hành động trong cùng một frame mà chúng ta đang điều chỉnh độ trong suốt hoặc thành phần nổi bật phản hồi lại sau một lần chạm, chúng ta sẽ không thấy hiệu ứng đó cho đến sau khi hàm onPress được gọi lại. Nếu onPress thực hiện lệnh setState và kết quả trả ra trong rất nhiều công việc đồng thời một vài frame bị bỏ qua, điều này có thể xảy ra. Giải pháp cho điều này là đưa các hành động trong trình xử lý onPress của bạn vào trong requestAnimationFrame:

handleOnPress() {
  // Always use TimerMixin with requestAnimationFrame, setTimeout and
  // setInterval
  this.requestAnimationFrame(() => {
    this.doExpensiveAction();
  });
}

Navigator chuyển đổi chậm

Như đã đề cập ở trên, Navigator animations được điều khiển trong luồng JavaScript. Hãy hình dung chuyển đổi "push from right": mỗi frame, phân cảnh mới được chuyển từ phải sang trái, sau đó offscreen (giả sử tại một x-offset là 320) và xử lý cuối cùng khi phân cảnh ở một x-offset. Mỗi frame trong quá trình chuyển đổi này, luồng JavaScript cần gửi một x-offset mới cho main thread. Nếu luồng JavaScript bị khóa, nó không thể làm điều này và vì vậy không có cập nhật nào xảy ra trên frame và animation.

Một giải pháp cho điều này là cho phép các animation chạy trên JavaScript-based để chuyển sang main thread. Nếu chúng ta làm tương tự như trong ví dụ trên với cách tiếp cận này, chúng ta có thể tính toán một danh sách tất cả các x-offsets cho cảnh mới khi chúng ta bắt đầu quá trình chuyển đổi và gửi chúng đến main thread để thực hiện một cách tối ưu . Bây giờ khi luồng JavaScript được giải phóng, nó không phải là một vấn đề lớn nếu nó giảm một số frame trong khi hiển thị cảnh - có lẽ bạn sẽ không để ý được bởi vì sự chuyển đổi khá nhanh.

Để giải quyết vấn đề này, mục tiêu chính là cần tạo ra một thư viện React Navigation mới. Các view trong React Navigation sửa dụng các thành phần native và thư viện Animated để hiển thị animaton với 60 FPS, đó là nó chạy trên native thread.

(Kết thúc phần 1) Trong phần 2 chúng ta sẽ tìm hiểu cách để hiển thị các thông tin và cách để đo Perfomance của ứng dung.

Nguồn tham khảo: Performance


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí