SwiftUI.BlurとLazyV(H)Stackの(描画パフォーマンス的)アンチパターン
はじめに
タップルでは、安心安全への取り組みとして「スクリーンショット・録画防止機能」を導入していました。
ある日、こんな報告が上がってきます。
「趣味タグをたくさん登録すると、探す面のスクロールがカクカクします」
調査の結果、主な原因はSwiftUI.BlurとLazyV(H)Stackの2つでした。
前提
前述した動作は、探す面に表示される趣味タグリストのことであり、
背景画像とユーザ画像(3枚分)が N行2列で並んだ画面のことです。
趣味タグを大量に登録したときに、
画像へのモザイク処理が大量に行われ、今回の報告が上がってきました。
なぜBlurは重いのか
- 毎フレームのリアルタイムぼかし
- view.blur(radius:) は動くたびにぼかしを再計算。
- スクロールやアニメ、サイズ変更で再描画→オフスクリーン描画 + ぼかしが都度走る。
なぜLazyV(H)Stackで重くなるのか
LazyV(H)Stackは、画面内+その前後の一部のViewを描画し、画面外に大きく外れたビューは描画しません。
その性質上、画面内にViewが入る(スクロール中)と同時に
画像取得(非同期処理)とモザイクの下処理が実行され、メインスレッドが圧迫されていました。
アプローチ方法 ① LazyVGrid, LazyVStackの廃止
LazyXXは、描画コストを抑えるために大量に描画する箇所では有効的です。
今回対象となった箇所では、最高でも20個のリストであり、画面内には最大で9個入るものです。
そのため、PrefetchなどのアプローチはせずにLazyXXの廃止を選択しました。
08/27:16:58追記
VGridで補完を探してしまって気づかなかったが、
Lazyでない`Grid`があったのでこちらの利用で良さそうでした。
N行2列表示のためのLazyVGridの使用をやめ、chunkによってデータ構造そのものを
2個ずつの要素を持つ多重配列にしました。
[1,2,3,4,5] → [[1,2],[3,4]]
stride(from: 0, to: self.count, by: chunkSize)
.map {
Array(self[$0..<Swift.min($0 + chunkSize, self.count)])
}
このようなデータ構造を渡すことで、View側は ForEachを使うだけで
簡単に N行2列を表現することができました。
結果、スクロールで「趣味タグリストが入ってくる時のカクつき」が無くなり、
モザイク機能を廃止していた頃よりもサクサク動くようになりました。
アプローチ方法 ② Blurの軽量化
スクロール時に .blur(radius:) をかけ続けるのはやはり重かったので、
「あらかじめぼかした画像を生成しておき、それを貼り付けるだけ」という方式に切り替えました。
工夫したポイント
- CoreImage でガウシアンブラーを一度だけかけ、NSCache に保存。
以降は画像を引っ張るだけなので描画負荷がほぼゼロになりました。 - 縮小してからぼかす
オリジナル画像をそのままぼかすのではなく、一度縮小(downsample)してからブラーを適用。
ピクセル数が大幅に減るのでフィルタ処理が数倍速くなります。 - 色空間変換の回避
CIContext のオプションを工夫し、不要なカラースペース変換をスキップ。
「なんかよくわからんけど CoreImage の初回だけ遅い」問題も、ウォームアップ処理を一度入れることで解消しました。
結果
- スクロール中にフレーム落ちしなくなる
- 見た目は従来のモザイクとほぼ同等
- モザイクを廃止していた頃よりもサクサク
と、期待以上の改善につながりました。
「Blurは重いから避ける」ではなく、工夫すれば十分実用的な軽さにできると分かったのは大きな収穫です。
また、描画パフォーマンス向上のためのLazy系で逆にパフォーマンスが悪化するケースもあるという、良い学びになりました。
採用について
カジュ面担当もしているので、中の様子が気になったら X(Twitter)まで気軽にDMください〜
タップルでは、新たな仲間を募集しております!
経験を最大化しながら、少子化などの社会問題の解決をめざし、
あなたの技術で世の中を幸せにしませんか?
ぜひ下記URLを覗いてみてください!
Discussion