烏龍@Concrnt(喪中)
taniryu-dragon.valdra.cc.draboros.net.ap.brid.gy
烏龍@Concrnt(喪中)
@taniryu-dragon.valdra.cc.draboros.net.ap.brid.gy
ドラゴンになりたいWebエンジニア(本職)。 相棒竜の名前はエリックといいます。 Twitter: https://x.com/drgn_oolong アイコン: みかづきさん( […]

[bridged from https://valdra.cc.draboros.net/ap/acct/taniryu_dragon on the fediverse by https://fed.brid.gy/ ]
サクッと使いたいときにのscreenは便利(だけどマルチカラムみたいな表示できるtmux/byobuはマスターしたい感ある)
November 25, 2025 at 8:46 AM
(何気に初めてコンカレから投稿してみる)
November 21, 2025 at 6:08 AM
> SSE(HTTPだけを使用して1リクエスト複数レスポンスする方法) これちょうどいい気がしてきた
November 21, 2025 at 6:08 AM
(???????
November 21, 2025 at 5:59 AM
世界の理をハックする方法……….
November 21, 2025 at 5:58 AM
アドベントカレンダー書きたいが、どんなネタがそれっぽいのか(?
November 21, 2025 at 5:57 AM
:nicehacking: が必要(?
November 21, 2025 at 5:54 AM
https://bsky.app/profile/gigazine.net/post/3m626zuieyy2k DownDetector’s Down Detector :rolling_on_the_floor_laughing:
November 20, 2025 at 7:26 AM
OSパッケージ版のWebサーバーにCVE対策されてるのかと聞かれると、調べるのが大変という話(
November 17, 2025 at 3:22 AM
PTRレコード引くとき、例えば `203.0.113.1` の逆引きを引くときに `1.113.0.203.in-addr.arpa` なのか `1.311.0.302.in-addr.arpa` なのか一瞬わからんくなる瞬間ある(
November 13, 2025 at 5:12 AM
せめて5gか4gかの表示は出してほしいな…
November 12, 2025 at 12:27 AM
Android16がiOS過ぎる
November 12, 2025 at 12:27 AM
(RAMは軽い)
October 30, 2025 at 11:55 AM
【Next.js】Next/Imageの画像プレビューにて発生したメモリリークを追う
この記事は、 PLEX Advent Calendar 2024の4日目の記事です。 ## こんにちは 株式会社プレックスでWebアプリケーションの開発をしているtetty0217 です。 ## はじめに 画像データを取り扱うフォームにおいて、入力した画像をプレビューで表示する機能が付属していることは珍しくないでしょう。 今日はそんなプレビュー機能の画像描画をnext/imageで無秩序に実装すると発生するクラッシュについて、Safariブラウザを題材として「なぜメモリリークが原因だったのか」「ブラウザでは何が起きていたのか?」という点をブラウザのレイヤー(Safari Web Inspector)とネイティブのレイヤー(Xcode)から追っていきます。 ## 対象読者 * Next.jsにおいて特に決まりなくnext/imageを使用している方 * next/imageによるメモリリーク発生時のブラウザや端末では何が起こっているのか気になる方 ## インデックス * 【破壊編】クラッシュを起こしてみよう * 【調査編1】クラッシュの原因を追う * 【調査編2】メモリリークの発生タイミングを追う * 【#メモリ君を救いたい編】様々なアプローチ検証 * まとめ ## 前提 大事ですね前提。本記事では主に下記を前提として検証をしています。 ### 計測について 各測定については3回行ったうちの中間の値を利用しています(パフォーマンス測定ツールについては観測ごとに差分が出るため) ### 検証環境 * 検証端末 * iPhone 15 Pro 18.1.1 * メモリリークがわかりやすいようにスマートフォン端末を使用 * ブラウザ * Safari * ツール * Xcode 15.4 * 検証アプリケーション * create-next-app@latest ### 検証アプリケーション bulk-image-preview.vercel.app ボタンをクリックしてから画像ファイルを選択すると、Blob化された画像データをGrid Layoutでnext/imageを通じて描画するだけのシンプルなアプリケーションです。 #### 他の検証パターン * next/image → imgタグへの置き換え * https://bulk-image-preview.vercel.app/img-tag * next/imageの描画数を3つに限定 * https://bulk-image-preview.vercel.app/narrow-image * 画像を圧縮してからBlob化 * https://bulk-image-preview.vercel.app/compressed-image #### 検証アプリケーションのコード github.com ## 【破壊編】クラッシュを起こしてみよう まずはこのGIFをご覧ください。 これは検証アプリケーションのホームアプリで1.4MBの画像ファイルを60枚入力し意図的にクラッシュを発生させたものです。 お手元にiPhoneをお持ちでしたら、実際に検証アプリケーションにアクセスして数十枚〜程度の画像ファイルを入力してみるのもよいでしょう。 昨今のスマートフォン端末のカメラアプリで撮影した写真データはサイズが大きく(※数MB程度)、ある程度のファイル数をBlob化したものをnext/imageで一挙に描画しようとするとこのようになります。 ## 【調査編1】クラッシュの原因を追う この章では破壊編において発生したクラッシュにはどのような原因があるのかをXcodeを活用して観測していきます。 ### メモリリーク...っぽいけど断定できる理由は何か でかいサイズのファイルを大量に描画しようとしてクラッシュしているんだから見ればわかるじゃないですかー。 まあ、それはそうなんですが、クラッシュした原因がメモリリークであるという論拠があってもいいですよね。 ということでXcodeのツールを使って探してみます。 ### なぜXcodeを使うのか クラッシュしたかどうかは実際の画面でもSafari Web Inspectorでもわかるのですが、システムがなぜクラッシュしたかというのはネイティブのレイヤーを見ないとわからないのです。 よって、Safari App自体のプロセスを観測するためにXcodeを使用します。 ### クラッシュ時のログを見にいく #### 1.準備 スマートフォン端末をMacBookと接続しておきます。 #### 2.コンソールアプリを開く Xcodeのメニューバーから `Window > Devices and Simulators` を選択します。 Devices and Simulatorsのウィンドウが開いたら、MacBookと接続している端末を選択し表示された `Open Console` を選択するとコンソールアプリが開きます。 #### 3.コンソールアプリでログを出力する コンソールアプリが開けたら `開始` を選択してから破壊編の動作を再度行います。 破壊編のクラッシュを引き起こしたところで停止をすると下記画像のように出力されたログを確認することができました。 #### 4.クラッシュ原因には何がある? クラッシュした原因を探そうにも何をどう見れば良いのか…ということでクラッシュレポートのタイプを特定するためにDeveloper AppleのUnderstanding the exception types in a crash reportを見ていきましょう。 ざっと見た限りではクラッシュ原因を分類するタイプが下記のように定義されています*1。 * EXC_BAD_ACCESS * 無効なメモリアクセス。解放済みポインタや無効なアドレスへのアクセスが原因で発生します。 * EXC_BAD_INSTRUCTION * 無効またはサポートされていないCPU命令の実行。通常はバグやアサーション失敗によるものです。 * EXC_BREAKPOINT * 明示的なブレークポイントまたはデバッガ停止命令が原因で発生します。 * EXC_CRASH * `abort()`などによる明示的なクラッシュの発生。 * EXC_RESOURCE * リソース制限(例: メモリ、CPU、ファイル記述子など)に達した場合に発生します。 * EXC_GUARD * ガードされたリソース(例: ファイルディスクリプタ)への不正な操作。 * EXC_CORPSE_NOTIFY * クラッシュ後のリソースの通知に関連する特殊な例外(通常、開発者が直接関与することは少ない)。 #### 5.クラッシュタイプの特定 先ほど出力したログにタイプをそれぞれ検索にかけてみると `EXC_RESOURCE` が引っかかりました。 どうやら「EXC_RESOURCE: com.apple.WebKit.WebContent exceeded mem limit: InactiveSoft 1536 MB 」というエラーが発生しています。 内容としては「WebContentプロセス(WebKitのレンダリングエンジン)が、メモリのソフト制限(InactiveSoft)1.5GBを超えている」というエラーです。 ### 調査報告 破壊編で発生したクラッシュの原因は**メモリリーク** であるということがわかりました。 ### 【調査編2】メモリリークの発生タイミングを追う 調査編1でクラッシュの原因はメモリリークだったことがわかりましたが、この章では時系列や統計情報で状況確認をしていくためにSafari Web Inspectorを活用して調査をしていきます。 ### Inspectorを実機に接続する #### 1. 準備 * スマートフォン端末とMacBookを接続しておく * MacBook側のSafariでは開発者モードを有効化しておくきましょう。 **Safariの開発者モード有効化**に関しては下記をチェック! product.plex.co.jp #### 2. 実機端末のWebインスペクタを有効化 * `設定 > アプリ > Safari` を開く * 画面最下部の `詳細` を開く * 表示された `Webインスペクタ` にチェックを入れると準備完了です #### 3. 実機のSafariブラウザとMacBookのWebインスペクタを接続 1. MacBookでSafariを開く 2. スマートフォン端末で検証アプリケーションを開く 3. Safariのメニューバーから `開発 > ${スマートフォン端末名称}` を選択する 実機のSafariブラウザとMacBookのWeb Inspectorを接続できました! 試しにalert関数を実行してみると実機側にも反映されます。 ### 破壊編の様子をツールで見てみる 実際にメモリリークが発生するまでをWeb InspectorのPerformance Timelineでプロファイルして見ていきましょう。 画像を入力してからFileをBlobに変換して描画をするまでを観測した図です。 CPUとメモリの使用率が急に0%になっている地点が破壊編で観測したメモリリークの発生箇所になります(崖になっているところ) ### Performance Timelineを見ていく WebkitのPerformance TimelineはChromeのPerformance Timelineより詳細度が低いですが、各DevToolsの中では情報がまとまって見やすいのではなかろうかと思います。 Timelineは各イベントを時系列でプロットしたイベントビューとイベントごとの詳細を表示する詳細ビューの2つのセクションに分かれています。 ※プロファイリング中はJITの最適化が解除されていることに注意が必要です。 #### スクリーンショット 観測範囲に時系列でViewPortのスクリーンショットを表示しています。 詳細ビューでは下記画像のようにViewPortの変遷を見ることができるのですが、どうやら入力した画像が描画され始めた辺りでメモリリークが発生したように伺えます。 #### ネットワーク要求 観測範囲において時系列で発生したネットワークのアクティビティを表示しています。つまるところネットワークタブをより簡潔にしたビューです。 今回は画像の入力のみを行っていますから、アプリケーションのAsset関連の取得を除くと画像のBlobデータが60個表示されます。 #### レイアウトとレンダリング 観測範囲において時系列で発生した描画と描画に関連する処理が列挙されます。 ここではブラウザ上での再レンダリングによるスタイルの再計算や60個の画像データの処理がコンポジットに表示されました。 #### メディアとアニメーション 観測範囲において時系列で発生したメディア要素とCSSのAnimationおよびTransitionの情報が列挙されます。 今回はスタイリングに使用したMUIの内部で処理されたTransitionが表示されています。 #### JavaScriptイベント 観測範囲において時系列で発生したJavaScriptのアクティビティやスタック、非同期処理、イベントの情報をサマリーとして列挙されます。 #### CPU 観測範囲において処理された関連するすべてのスレッドの情報のサマリーが列挙されます。 ここではどのスレッドでどの程度のCPUが使用されたのか、処理のピーク、メインスレッドの各アクティビティの割合など統計を確認することができるので、パフォーマンスを確認する上でまず見るべきタイムラインになってきます。 #### メモリ 観測範囲におけるメモリ使用量の大枠が内訳が表示されます。 上記のような確認を経て、ブラウザのクラッシュ発生時は「ViewPortに画像が描画され始めた箇所」でCPU/メモリの使用を維持できずに(クラッシュしたことで)Web Inspector上での観測が中断されたことがわかりました。 ### タイムラインの内容を分析する もうnext/imageの箇所直して終わりじゃん!いいえ、まだまだ追っていきます。 今回はメモリリークが原因ということで特に数値が大きいCPU Timelineのプロファイルにフォーカスして見ていきます。 #### 使用率の内訳 このようにCPU Timelineの詳細では観測範囲における総計を見ることができます。 メインスレッドは40%弱使用している中、その他(WebKit thread + Other thread)ではなんと200%以上もCPUを使用しています。 比較のためnext/image → imgタグに置き換えた画面を使用してみると下記のような結果になりました。 メインスレッドは大きく変わっていませんが、その他のスレッドの特にWebkit ThreadがCPU使用率が激減しています。 #### Webkit Threadとは WebKitが持つバックグラウンドの最適化処理などを実行するスレッドのまとまりを表しているものです。 ### 調査報告 今回の主題である画像の最適化処理に関して、Webkit2にはバックグラウンドで処理を実行してブラウザのレスポンスに影響をなるべく及ぼさないようにする**split process model** という概念がありました。 trac.webkit.org next/imageのようにimgタグに対して画像描画の最適化を図るオーバーヘッドを持っているようなコンポーネントを多く描画することがこの別スレッドを大きく使用することになるみたいですね(もちろん画像サイズも。) Webkitあたりはちょっと記憶が薄いところを掘り起こしてきましたので、情報が古かったり誤っていたら是非ご指摘をいただけると幸いです。 ## 【#メモリ君を救いたい編】様々なアプローチ検証 この章では破壊編と同じデータ量を使い、画像の加工方式や描画量、next/image以外を使うなどアプローチを変えて事象を見てみます。 ### 【パターン1】next/imageからimgタグに置き換える next/imageは上述の通り、imgタグを描画するにあたってさまざまな最適化を行うことから画像の描画にあたってのオーバーヘッドが乗っているコンポーネントになります。 今回のプレビュー機能にはユーザーに対し、これから投稿される画像を「事細かに確認してほしい」というメッセージはありません。 よって選択した画像が大まかにあってそうか確認してもらえればよいという解釈の元、next/imageではなくネイティブのimgタグを使用してみました。 https://bulk-image-preview.vercel.app/img-tag 結果としては同じデータ量にも関わらずクラッシュすることはありませんでした。 これは調査編2で観測した通り、CPUとメモリの使用率が大きく下がったことが要因としてあります。 そのため、試しに同じサイズの画像ファイルを200件に増やして追加してみても無事描画されました。 ### 【パターン2】入力サイズは同じだが描画範囲を減らしてみる サイズの大きいリストを表示するためには、画面のパフォーマンスと体験を両立させるために仮想スクロールといったDOMの描画を制限するという手法があります。 next/imageとデータ量は同じとして描画量を減らしてみるとどうなるでしょうか?簡単に描画するnext/imageを3つに減らしてみましょう。 https://bulk-image-preview.vercel.app/narrow-image **CPUタイムラインの総計** 描画するDOMを激減させたので、それは大丈夫でしょうと思われるかもしれません。私もそう思います! 実際に処理される画像データは変わりありませんが、next/imageが描画される量によって違いがあるということで、やはりnext/imageをたくさん使うと大変だということがわかります。 imgタグに対して行った200件を追加してみると、少し時間がかかりつつも描画されたのでnext/imageのオーバーヘッドがいかに大きいかがわかります。 ### 【パターン3】画像ファイルを圧縮してからBlob化する 最後に、単純にnext/imageが描画される量が単に多いだけがメモリリークの理由なのか?ということを見ていきたいと思いますので、Blob自体のサイズを削減してみましょう。 今回は例としてblueimp-load-imageを利用して画像ファイルの圧縮を行います。 https://bulk-image-preview.vercel.app/compressed-image **CPU Timeline** どうやら、サイズの大きいBlobほどnext/imageにおけるオーバーヘッドが大きくなるようですね。 圧縮処理を追加しているため破壊編の例よりはメインスレッドの使用率が高く、Blobサイズが小さくなったためnext/imageのオーバーヘッドが小さくなっていることがわかります。 ちなみに限界まで試したところ150件でOOM(Out of Memory)がかかりました。 ### 調査報告 メモリリークを引き起こさないためには色々なアプローチが取れそうだということがわかったので、サービスの体験に応じて最適化を考えていければと思います。 本記事では詳しく言及しませんが、ファイルのBlob化においてデータが不要になったタイミングでメモリへの参照を破棄しておくことです。 createObjectURLによってBlob化されたデータがunloadのタイミングで自動破棄されるまではメモリへの参照が維持されますから、アプリケーションを途中で落とさないためにも破棄するBlobに対してrevokeObjectURLをしておくことは必須です。 developer.mozilla.org ## まとめ ここまで読んでくださりありがとうございました。 本記事では、Next.jsのnext/imageを使用した画像プレビュー機能で発生するメモリリークについて遠回りや深追いをしてみました。 今回のような雑多な調査は時間がかかりますし、仕事の面では使う機会の少ない知識が多く入ってきます(※もちろんそうではない職域やポジションはあると思います。) しかし、問題に対して仮説検証の末すぐに解決するのではなく、たまには事象そのものを深く追ってみたり、違う視点で見てみるのもよいのではないでしょうか? 今後のエンジニア人生でヒラメキの材料になったり技術トークの良いネタになるでしょう。 PS.本記事でお気づきの点があればぜひコメントください! ## 最後に 現在プレックスではソフトウェアエンジニア、フロントエンドエンジニア、UIデザイナーなど各業種を募集しています。 メンバー全員で行動や技能の基準値を上げていくことで強い組織を目指しています。 一緒に働いてみたいと思った方がいましたら、是非ご連絡をお待ちしています! 埋め込み[dev.plex.co.jp] ## 関連リンク ### Safari Web Inspector developer.apple.com ### Webkit webkit.org webkit.org trac.webkit.org ### Developer Apple developer.apple.com developer.apple.com developer.apple.com ### GitHub github.com ### Next.js nextjs.org *1:ChatGPTによってドキュメントの内容を簡略化してもらいました
product.plex.co.jp
October 30, 2025 at 11:38 AM
Safari、画像を扱うとメモリリークしがち(困った)
October 30, 2025 at 11:23 AM
というか、勘違いを誘発するようなサイトの紹介になってて本当に頭にくるわ。
October 27, 2025 at 8:51 AM
最初聞いてた決済システムの仕様と、実際の仕様が全然違ってる可能性が出てきてブチギレてる
October 27, 2025 at 8:51 AM
https://x.com/gigazine/status/1981549032389382542 大規模システムは大変だ…..
October 24, 2025 at 3:16 AM
(世の中にはサーバーの熱でお風呂を沸かしているデータセンターがあるとかないとか)
October 22, 2025 at 3:26 AM
自宅サーバー、暖房器具になるは割とあるある(
October 22, 2025 at 3:25 AM
自宅サーバー波来たか…?
October 22, 2025 at 2:31 AM
悩みQueueに一つ悩みが増えた _(´ཀ`」 ∠)_
October 21, 2025 at 5:11 AM