KiSAKU
技術系7分で読めます

上げる先が、ひとつしかなかった

公開

脆弱性の警告が28件。そのうち23件が next に集約されることまでは分かっていました。あとは上げるだけ——のはずが、まず「どこまで上げるか」で手が止まりました。

この記事で扱うこと

更新先のバージョンをどう選んだか、自動化ツールがやってくれたこととやってくれなかったこと、そして「入れる必要のない設定」を消した判断。メジャーバージョンを2つ跨ぐ更新を、確認しながら進めた記録です。

まず、選択肢を数える

手元は 14.2.35 でした。自動で提案されていたのは 16.3.1。2世代ぶんの差があります。

最初に考えたのは「まず14系の最新まで上げて、それで消える分を消そう」でした。メジャーを跨がなければ壊れにくいからです。これは実行しませんでした。調べたら、前提が違っていたからです。

サポート状況
Next.js 14  サポート終了(2025-10-26)
            最終パッチ 14.2.35 ← 手元にあるのがこれ

Next.js 15  Maintenance LTS  2026年10月まで
Next.js 16  Active LTS       2027年10月まで

14系の最新は、すでに手元にありました。「上げる」という操作に余地が無く、18件の警告は14に留まる限り1件も消えません。

やっても何も起きないコマンドを、実行しかけた

調べる前は「14系の最新に上げましょう」と判断していました。実行すれば何かが変わるはず、という思い込みです。手を動かす前にサポート期限を1つ調べただけで、この無駄が消えました。作業の前に前提を確認するのは、遠回りに見えて速いです。

15を選ばなかった理由

残る選択肢は15と16です。15の方が変更が小さいので、一見こちらが安全に見えます。

しかし15のサポートは2か月後に切れます。そこで上げても、すぐ同じ作業をやり直すことになります。「変更が小さい」ことの価値は、その変更が長持ちするかどうかで決まります。2か月で無効になるなら、小さいことに意味がありません。

実質的に、上げる先は16しかありませんでした。

何が壊れるかを、先に調べる

移行ガイドを読んで、このサイトに該当するものだけを抜き出しました。全部を読むのではなく、自分のコードに関係するものを探すという読み方です。

変更点 このサイトへの影響
params が非同期になる 該当。記事ページで同期的に使っている
React 19.2 が必要 該当。18.3.1 から上げる必要がある
next lint の廃止 該当。package.json に書いてある
画像設定の既定値変更 明示指定していないので影響なし
Turbopackが既定に webpack設定を持っていないので影響なし
middleware の改名 使っていない

該当したのは3つだけでした。2世代ぶんの変更に見えて、実際に手を入れる場所は限られていると分かった時点で、作業の見通しが立ちます。

自動化ツールは、半分やってくれる

Next.jsには移行用のコードモッド(コードを機械的に書き換えるツール)が用意されています。2つ実行しました。

実行したもの
# パッケージ更新・設定ファイルの書き換え・lintの移行
npx @next/codemod@canary upgrade latest

# params / searchParams の非同期化
npx @next/codemod@canary next-async-request-api .

変換結果は正確でした。たとえば記事ページはこうなります。

自動変換された箇所
- type Props = { params: { slug: string } };
+ type Props = { params: Promise<{ slug: string }> };

- export default async function BlogPostPage({ params }: Props) {
+ export default async function BlogPostPage(props: Props) {
+   const params = await props.params;

49ファイルを走査して2ファイルを変更、エラー0。手で直していたら見落としが出る種類の作業です。

変換されなかった場所も、確認する

「0 errors」は「全部やった」ではありません。ツールが認識できなかった書き方は、静かに素通りします。変更されたファイル数と、自分が想定していた箇所の数が合っているかは、自分で確かめる必要があります。今回は2ファイルで一致していました。

入れる必要のない設定を、消した

ここが今回いちばん判断が要った箇所です。

コードモッドは、7つのファイルにこれを追加していました。

追加されていたもの
// TODO: Cache Components adoption. Refactor this route so this
// opt-out can be removed.
export const instant = false;

調べると、これは16.3の新機能に対するオプトアウトで、別の設定を有効にしているときにだけ意味を持ちます。このサイトはその設定を使っていないので、7ファイルすべてで何もしていません。

将来のために先回りして入れてくれたものです。しかし残すとどうなるかを考えました。

残す場合 消す場合
将来その機能を有効にしたとき、警告が出ない そのとき改めて入れる必要がある
「あとで対応する」というTODOが7か所に残る コードに無効な設定が残らない

消しました。誰も対応しないTODOは、そのうち景色になります。読むたびに「これは何だっけ」と確認するコストが7か所ぶん発生し、いずれ誰も読まなくなります。必要になったときに、必要な場所へ入れるほうが健全だと判断しました。

自動生成されたものを、そのまま受け取らない

コードモッドが入れたものは正しい提案です。しかし「このプロジェクトにとって必要か」までは判断していません。ツールは一般的に安全な側へ倒します。プロジェクトの事情を知っているのは書き手のほうです。

本番に入れる前に、本番と同じもので確かめる

今回はmainに直接入れず、ブランチを切りました。

進め方
git checkout -b feat/next-16
(コードモッド実行・手直し)
npm run build      ← 手元で通ることを確認
git push -u origin feat/next-16

ブランチをpushすると、デプロイ先がプレビュー用のURLを作ってくれます。本番と同じビルド手順で、本番と同じ環境に載った状態を、公開せずに触れます。

そこで確認したのは6点です。

確認項目
1. 記事一覧が20本、サムネイルが出る
2. 記事ページの本文・目次・読了バー
3. sitemap.xml が25件(数を数える)
4. robots.txt が4行
5. 構造化データの日時に Z が付いている
6. ビルド時のエラー・警告
3番は、数えないと分からない

sitemapは、失敗しても固定ページだけを返して200で応答する作りにしてあります。壊れていても画面は正常に見えます。「表示された」ではなく「25件あった」まで確認しないと、記事がsitemapから消えていても気づけません。

全部通ったのを見てから、mainへマージしました。プレビューで通ったものが本番でも通る——確認の順番として、これが一番安く済みます。

結果

脆弱性の件数
next     18件 → 0
postcss   4件 → 0
nanoid    1件 → 0
glob      3件 → 0
lodash    2件 → 2   (開発用のみ)

合計     28件 → 2件

glob が消えたのは想定外でした。ESLintも一緒に上がり、その依存として新しい glob が入ったためです。本番で動くもののうち、既知の脆弱性は無くなりました。

残った2件は開発用の依存で、配信される成果物には入りません。

今回の学び

まず、「上げる」の前に、上げ先を数えること。14系の最新はすでに手元にあり、実行しても何も起きないコマンドを打ちかけていました。サポート期限を1つ調べるだけで防げます。

次に、変更の小ささより、その変更が持つ期間で選ぶこと。15は変更が小さいですが2か月で切れます。安全に見える選択肢が、実は先送りなだけということがあります。

そして、自動化ツールの出力も、読んでから受け取ること。コードモッドは正確でしたが、このプロジェクトに不要なものも入れていました。ツールは一般的に安全な側へ倒し、個別の事情までは知りません。

最後に、本番と同じもので確かめてから本番に入れること。プレビュー環境を1回挟むだけで、「マージしてから気づく」が「マージ前に気づく」に変わります。確認の順番を変えただけで、リスクの大きさが変わります。

← ブログ一覧へ戻る