こんにちは、スマートニュース株式会社の紀平です。

SmartNews のアプリ内では、最近 WebView を使った機能を多数公開しております。そのうちの一つにショッピングチャンネルという機能があるのですが、今日はそのショッピングチャンネルのタイムセール枠で以前発生した、謎のプチフリーズ問題の調査手法をご紹介します。

tl;dr

  • 特定の Android 端末でプチフリーズが発生した
  • 再描画領域を減らすことで現象は改善した
  • Chrome の Tracing 機能を利用し、ブラウザのソースコードまで参照して、ついに根本原因まで突き止めた

タイムセールで起こった問題

タイムセールは、SmartNews のショッピングチャンネル内において WebView で提供されている、期間限定のお得な商品をアグリゲーションした機能です。EC サイト各社のご協力の上にご提供しております。内部は HTML5 で開発されております。

こちらの画像は2020年11月13日段階のものとなりますが、最新版の SmartNews アプリでショッピングチャンネルを開いて頂くと、ページ内にアイテムが多数表示され、各アイテムはそれぞれ販売終了時間を保持している様子をご確認いただけます。販売終了時間間近になるとこれらがカウントダウンを表示し、ゼロになると販売終了シールが貼られます。

商品の数は日によって大きく変わりますが、商品が 100 個や 300 個並ぶことも珍しくありません。また 1 日の終わり(ゼロ時)に販売終了時間になる商品が多く、夕方になると多数のアイテムがカウントダウンを表示することになります。

とすると気になるのが、カウントダウンの際の重さです。カウントダウンが複数重なった際に処理が重くなる可能性は開発時に認識しており、どれくらい重くなるか試行したのですが、問題のない範囲という観測結果であったので、当初は特に大きな改善を入れることなくリリースしました。

ところが、リリース後すぐに社内ユーザーの一人から「夕方になると重くて使い物にならなくなる」という報告が入りました。実際に使用していた端末(OnePlus)を見せてもらうと、あり得ない程の重さになっていました。スクロール中に顕著なのですが、小さなフリーズ(プチフリーズ、プチフリ)が頻発してスクロールがその都度止まってしまい、ユーザー体験は極めて悪い状況でした。

決して端末のスペックは悪くないのに、なぜ特定の端末のみでここまで重くなるのか…。ブラウザ間の挙動の違いに遭遇するとテンションが上がるエンジニアとして、このような現象が起こる原因に大きく心が惹かれましたので、巻き取って調査することにしました。

原因調査

WebView のリモート inspect

ということで、基本通りまずプロファイルを取りました。

スマートフォンの上の Chrome ブラウザの inspect は、デバッグモードを on にした端末を PC と USB 接続することで出来ることをご存知の方が多いと思いますが、WebView であっても基本的には同じ様に調査することが出来ます。ただし、Native アプリ側でデバッグを許可する必要があるのに注意です。

WebView.setWebContentsDebuggingEnabled(true);

デバッグビルドの SmartNews アプリに上記の設定を入れることで、USB 接続した PC から WebView 内部のブラウザの inspect をすることが可能になりました。

正常端末との比較

問題なく inspect が出来るようになったので、次に正常端末と問題端末でどのようなパフォーマンスの違いがあるのか確認しました。カウントダウンが 300 ほどあるページを表示したときの、それぞれの結果がこちらです。

問題のない端末(Pixel 3)のパフォーマンス
問題のない端末(Pixel 3)のパフォーマンス
OnePlus のパフォーマンス
OnePlus のパフォーマンス

当時作業ログを記録していた slack のチャンネルより転載しました。見てわかるように、カウントダウンの際には Pixel 3 においても高負荷の処理が発生しております。Pixel 3 だと、アイテムが 300 個あっても 60 ミリ秒強(フレームにして 4 フレーム弱)で終わっているので、ユーザー体験をそこまで損ねていなかったようです。

一方で OnePlus の場合、同じ処理に全部で 230 ミリ秒ほどかかっておりました。カウントダウンは毎秒発生するので、毎秒 14 ほどスクロール等が止まる計算になります。このように書くと、非常にユーザー体験が悪いことをご理解いただけるかと思います。

パフォーマンスの深堀り

さて、上記のパフォーマンスをもう少し詳細に見てみましょう。まずは Pixel の方です。

時間がかかっているのは Layout であり、その直前に Rec...yleRecalculate Style)が見えます。タイムセールの HTML はアイテム数が多く縦に長い構成なのですが、そのアイテムにおいて満遍なく 300 ヶ所の文字列変更を JavaScript で入れており、それがここの部分で重い処理となっております。

この問題は、現在表示されているアイテムに限ってカウントダウンを更新することで解決できました。更新の発生する箇所(dirty rect と呼ばれることもあります)を少なくすればするほど、この Layout (Firefox では reflow とも呼ばれます)にかかる時間を減らすことが出来ます。本件に関しては Reflow の発生自体を止めることはあまり現実的ではなかったので、なるべく影響範囲を減らす方向で対応することが出来ました。

Layout / Reflow 系の話は、こちらの記事が参考になります。

OnePlus 特有の問題

上記と同じ問題は OnePlus でも適用出来ます。更新場所を減らし、Layout にかかる時間を減らすことで、OnePlus でもスムーズなスクロールを担保することが出来ました。 プチフリーズの問題自体はこれで解決した、と言って差し支えないでしょう。

しかし、たかだか 300 程度のカウントダウンにも関わらず、OnePlus で異常に時間がかかっているのは何が原因なのか、というのを突き止めるが今回の調査のゴールです。既に解決策は機能しているので、ここから先の調査は本来必要ないかもしれませんが、「OnePlus でのみ問題が起こっていたのはなぜなのか」を明確にしたい、という思いで調査を開始したので、可能な限り問題の原因を追ってみようと思いました。

というわけで、再度プロファイル結果を見てみましょう。上のスクリーンショットで、あからさまに謎の時間がかかっている領域が見て取れます。拡大して見てみましょう。

OnePlus スクリーンショット再掲、拡大して
OnePlus のスクリーンショット再掲、拡大して

この部分です。168.66 ミリ秒もかかっており、今回の大戦犯です。このような長時間の処理は Pixel 3 では確認されませんでした。ここが OnePlus におけるプチフリーズの原因であるのが明確ではあるものの、DevTools の Performance では Task としか表示されておらず、これをクリックしても有用な情報は出てきませんでした。

普段の開発においてこのような現象に出くわすことは滅多にありませんが、私達は年に 1〜2 回くらいの頻度で巡り合うことがあります。こういう時、さらに深く調査する方法をご紹介しましょう。

Tracing を使った調査

Chrome のコアな開発者向けの機能として、 chrome://tracing というものがあります。DevTools 以上に詳細な、Chromium のソースコードレベルまで踏み込んで原因を探りたい場合に重宝する機能です。今回は DevTools では原因がはっきりわからなかったので、tracing を使って調査をしてみました。

Tracing とは

Tracing とは、ブラウザ実装から出力される詳細なログを追う機能です。Tracing の機能自体はブラウザに限定されるものではないのですが、ここではブラウザに限定した話としてご紹介します。

Tracing は DevTools の Performance 以上に詳しい情報が必要な際に助けになってくれることがあるので、知っておくといざという時に役立つかもしれません。特にブラウザのバグが疑わしい場合などに、この機能を利用すると実際のブラウザのソースコードと関連付けて調査することが出来ます。

Tracing の初期化

Tracing を PC で使う場合、以下の URL から呼び出します。

chrome://tracing

このように呼び出すと、今表示されているすべての chrome のタブについての情報を一気に集めることが可能です。Incognito モードであっても対象になります。重い調査をしてしまうと、ブラウザのタブ全部を巻き込んで落ちるので注意してください。PC 版で調査する際には、私は Chrome Canary など別のブラウザインスタンスを利用して調査しております。

今回は PC ではなく、モバイル端末の Chrome(WebView)について調査をする必要があります。モバイル端末の場合は、USB 接続をした PC より以下の URL から呼び出します。

chrome://inspect?tracing

そうすると、普段の inspect の画面に trace というリンクが見えると思います。

これをクリックすることで、モバイル端末に対して Tracing を呼び出すことが出来ます。

Tracing で記録を開始する

Tracing の画面が出たら、Record ボタンを押して準備を開始します。どのログを収集するかを聞かれますが、普段は “Web Developer” を選んでおけばよいでしょう。そして Record を押して記録を開始し、Stop を押して記録を終了します

tracing の設定画面
tracing の設定画面

無事に記録が終わると、PC の画面上で詳細なログを時間軸で確認することが出来ます。

このように、色々な情報が取れているのがおわかりになるかと思います。少し癖のある UI ですが、マウスツールを駆使して目的の場所を探します。今回は Timesale タブのレンダリングプロセスの CrRendererMain が対象になります。

ここの見た目は DevTools に似ているので、直感的にわかりやすいと思います。では問題となっている箇所を拡大してみましょう。

このように表示されました。 ThreadControllerImpl::RunTask をクリックすると、その引数を表示することが出来ます。今回は render_accessibility_impl.cc が表示されておりますね。

え…?accessibility??? なんで?

Chromium のソースコードまで追ってみる

ちょっと意味不明だったので、Record 対象のイベントを広めに設定してみた結果がこちらです。

この結果から判断する限りは Accessibility が問題であることはほぼ確定で、その中でも SendPendingAccessibilityEvents が一番時間を食っていることがわかりました。なぜアクセシビリティ機能がこんなに時間を食っているのかさっぱり不明ですが、なにはともあれソースを覗いてみましょう。調査している時の WebView の Chrome バージョンが Chrome (78.0.3904.108) であったので、そのバージョンのソースコードを覗いてみました。

https://chromium.googlesource.com/chromium/src.git/+/78.0.3904.108/content/renderer/accessibility/render_accessibility_impl.cc#472

このソースを見る限り、重くなるとすると、

  • 何らかのアクセシビリティ機能がオンになっている( pending_events_ が存在する)
  • 更新のある場所(dirty rect)が大量にある

という条件を満たしているらしいことがわかります。

原因を確定させる

半信半疑ながら、Accessibility の設定が重くなっている要因であることを確定させることにしました。OnePlus 端末で関係していそうな設定を色々と変えてみたのですが、どのように変更しても重い処理が改善することはありませんでした。

そこで、HTML 側を変更することによって実験することにしました。最近の HTML にはアクセシビリティ機能を制御する属性があるので、試験的にアクセシビリティ機能をオフにして問題が発生するかどうかをテストしてみました。

<body aria-hidden="true">

そしてなんと、アクセシビリティ機能をオフにすると、 OnePlus で発生していたプチフリーズが発生しなくなりました。本当にアクセシビリティ機能がこの問題の原因だったのです。

なぜ OnePlus の端末で常に Accessibility の機能がオンの状態で Chrome が呼ばれていたのかについては未調査です。端末側の何らかの設定かもしれませんし、その端末固有の設定の問題だったのかもしれません。ただ現実として、OnePlus においてアクセシビリティ機能のオン・オフが Android の WebView のレンダリングに大きな影響を与えていることは間違いのない事実であり、Tracing 機能を使うことによってそれを突き止めることができました。

既に更新場所を減らすことで問題自体は解決していたのですが、OnePlus のみで発生していた原因まで特定出来て、個人的に大変良い経験になりました。

(追記)

誤解を招かないように強調させていただきますが、「アクセスビリティ機能をオフにして解決した」のではなく、「原因がアクセスビリティ機能であったことを突き止めた」という話でした。問題自体はこの記事の序盤でお伝えしたとおり、dirty rect の削減によって解決されております。

まとめ

私はネイティブアプリにおける WebView の使用は、いわばアプリの中にエミュレータのようなものが入っていて、そのエミュレータの上でプログラムが動いているのに近い状態だという風に考えております。基本的に大変便利なものなのですが、エミュレータ故の問題が色々と起こります。

そして現実のブラウザは、エミュレータよりももっと複雑であり、また多数の変数によって挙動の変わる繊細なものでもあります。ちょっとした機能を作る場合に WebView が最適でも、機能が複雑になればなるほど開発が難しくなります。WebView で機能を実装するときには、そういった問題に取り組む覚悟が必要です。

幸いなことに、Chrome ではソースコードにアクセス出来ますし、様々なツールによって詳細な調査をすることが可能です。おかげでブラウザをブラックボックス化することなく、ライブラリのように深いところまで調査することが出来ます。今回もソースコードレベルでのプロファイルが取れたおかげで、予想もしなかった原因を突き止めることが出来ました。

WebView を用いた開発には、良い面もあれば悪い面もあります。私達は WebView の良い面を最大化し、悪い面が出ても全力でそれを解決することを目標にしております。SmartNews は特にユーザー体験にこだわりを持ったアプリですので、ブラウザの利用によってその体験が損なわれないように気をつけなければいけません。そのこだわりは、時には今回のように大変楽しい経験をもたらしてくれます。

Web フロントエンドの開発では中々深いところまで潜る機会がないのですが、スマートニュースでは幅広いユーザーの皆様からの多数のフィードバックを元に最先端の機能を活用した開発を行っていく中で、ときおりこのような案件が発生します。もしこの記事をみてワクワクされた方は、是非私達と一緒に開発することを検討してみてください。フロントエンドの開発キャリアにおいて、きっと良い糧になると思います!

We’re hiring

私達 WebTech team は、SmartNews というネイティブアプリの中で Web 技術を使った機能開発等に注力しております。ご興味のある方は、ぜひお気軽に私達までコンタクトしてください!

https://apply.workable.com/smartnews/j/1B23E54D1C/