著色器編譯時卡頓

If the animations on your mobile app appear to be janky, but only on the first run, this is likely due to shader compilation. Flutter’s long term solution to shader compilation jank is Impeller, which is in early developer preview (behind a flag) on the master channel for iOS. (It’s not yet available on Android.) Before continuing with the instructions below, please try Impeller on iOS, and let us know in a GitHub issue if it doesn’t address your issue. Impeller on Android is being actively developed, but is not yet in developer preview.

While we work on making Impeller production ready, you can mitigate shader compilation jank by bundling precompiled shaders with an iOS app. Unfortunately, this approach doesn’t work well on Android due to precompiled shaders being device or GPU-specific. The Android hardware ecosystem is large enough that the GPU-specific precompiled shaders bundled with an application will work on only a small subset of devices, and will likely make jank worse on the other devices, or even create rendering errors.

Also, please note that we aren’t planning to make improvements to the developer experience for creating precompiled shaders described below. Instead, we are focusing our energies on the more robust solution to this problem that Impeller offers.

What is shader compilation jank?

A shader is a piece of code that runs on a GPU (graphics processing unit). When the Skia graphics backend that Flutter uses for rendering sees a new sequence of draw commands for the first time, it sometimes generates and compiles a custom GPU shader for that sequence of commands. This allows that sequence and potentially similar sequences to render as fast as possible.

Unfortunately, Skia’s shader generation and compilation happen in sequence with the frame workload. The compilation could cost up to a few hundred milliseconds whereas a smooth frame needs to be drawn within 16 milliseconds for a 60 fps (frame-per-second) display. Therefore, a compilation could cause tens of frames to be missed, and drop the fps from 60 to 6. This is compilation jank. After the compilation is complete, the animation should be smooth.

On the other hand, Impeller generates and compiles all necessary shaders when we build the Flutter Engine. Therefore apps running on Impeller already have all the shaders they need, and the shaders can be used without introducing jank into animations.

要獲得更加確切的著色器編譯卡頓存在的證據,你可以在 --trace-skia 開啟時檢視追蹤檔案中的 GrGLProgramBuilder::finalize。下面的截圖展示了一個 timeline 追蹤的範例。

A tracing screenshot verifying jank

如何定義「首次執行」?

在 iOS 上來說,「首次執行」意味著使用者可能在每次開啟應用後,在動畫首次載入時都會出現卡頓。

如何使用 SkSL 預熱

在 1.20 釋出的時候,Flutter 為應用開發者提供了一個命令列工具以收集終端使用者在 SkSL(Skia 著色器語言)進行格式化處理中需要用到的著色器。 SkSL 著色器可以被打包進應用,並提前進行預熱(預編譯),這樣當終端使用者第一次開啟應用時,就能夠減少動畫的編譯掉幀了。使用下面的指令收集並打包 SkSL 的著色器:

  1. ​ 開啟 --cache-sksl 執行你的應用以捕獲 SkSL 中的著色器:

    flutter run --profile --cache-sksl
    

    如果這個相同的應用之前執行的時候沒有使用 --cache-sksl,你需要加上 --purge-persistent-cache 標誌:

    flutter run --profile --cache-sksl --purge-persistent-cache
    

    這個標誌將會刪除可能干擾 SkSL 的較舊的非 SkSL 著色器快取捕獲的著色器。它還清除了 SkSL 著色器,因此在第一次使用 --cache-sksl 執行。

  2. 儘可能多觸發應用的動畫,特別是那些會引起編譯卡頓的。

  3. 在執行 flutter run 命令後行按下 M 鍵以捕獲 SkSL 著色器到一個類別似 flutter_01.sksl.json 的檔案中。為了達到最好的效果,最好是能夠在 iOS 真機上抓取 SkSL 著色器,在模擬器上的抓取通常會是無效的。

  4. Build the app with SkSL warm-up using the following, as appropriate:

    在下面的命令中選擇合適的建構帶有 SkSL 預熱的應用:
    
    flutter build ios --bundle-sksl-path flutter_01.sksl.json
    

    如果它會建構一個類別似 test_driver/app.dart 的驅動測試,請確保指定 --target=test_driver/app.dart。(例如 flutter build ios --bundle-sksl-path flutter_01.sksl.json --target=test_driver/app.dart

  5. Test the newly built app.

或者,你可以編寫一些整合測試來使用一個命令自動執行前三個步驟。例如:

flutter drive --profile --cache-sksl --write-sksl-on-exit flutter_01.sksl.json -t test_driver/app.dart

使用這樣的 整合測試,無論是程式碼發生改變或者 Flutter 更新了,你都可以輕鬆獲得可靠的著色器快取。這些測試也被用於驗證開啟著色器預熱前後的效能變化上。更好的做法是,你可以把這些測試放進 CI(持續整合)系統上,這樣就能在每次應用釋出前自動產生並測試著色器快取了。

就拿原始版本的 Flutter Gallery 舉例。我們讓 CI 系統在每次 Flutter commit 後都產生著色器快取,並在 transitions_perf_test.dart][] 中驗證效能。更多詳細資訊請檢視 Flutter Gallery sksl 預熱過渡效能驗證,以及 Flutter Gallery sksl 預熱過渡在 iOS_32 上的效能驗證

在這種這種整合測試中,最差的幀光柵化時間是一個很好的指標來衡量著色器編譯卡頓的嚴重性。例如,上述步驟減少了 Flutter gallery 應用的著色器編譯卡頓,並減少了它在 Moto G4 手機上的最差的幀光柵化時間,從 ~90 ms 減少到 ~40 ms。在 iPhone 4s 上,它從 ~300 ms 減少到 ~80 ms。這種視覺差異如同本文開頭所示一樣。