LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

Vue Fes Japan 2026 クイズ解説

こんにちは、今年新卒で入社し、LINEギフトで主にバックエンド開発を担当している中西です。学生時代はVue.jsを使った開発に取り組んでおり、Vue Fesには過去2年、参加者として足を運んできました。今回は、参加者として感じたことを生かしたいと考え、ブースの企画・運営に携わりました。

2026年10月24日に開催された「Vue Fes Japan 2026」にて、LINEヤフー株式会社は昨年に続きゴールドスポンサーとして参加しました。LINEヤフーの企業ブースでは、「Vue Quiz」と「LINEギフトからの挑戦状!」の2つのテーマでクイズを実施しました。この記事では、両テーマで出題した全4問の正解と解説を紹介します。

LINEヤフーの企業ブースに展示した「Vue Quiz」と「LINEギフトからの挑戦状!」の出題パネル

Vue Quiz

「Vue Quiz」では、Vue.jsや周辺技術の最近の動向をどこまで把握しているかを問う知識問題を2問出題しました。

Q1

Vite 8で、それまでesbuild(開発時)/Rollup(本番ビルド時)が担っていた役割を統合し、標準のバンドラとして採用されたRust製プロジェクトの名前はどれでしょうか?(1つ選択)

  • A:Turbopack
  • B:esbuild
  • C:Rolldown
  • D:SWC

正解・解説

正解は C:Rolldown です。

Vite 7までは、開発時の依存関係の事前バンドルをesbuild、本番ビルドをRollupが担っていました。Vite 8では、これらをRust製のバンドラであるRolldownに統合しています。

ただし、esbuildが担っていた処理のすべてがRolldownに移ったわけではありません。標準構成では、JavaScriptの変換やminify(圧縮)は、同じくRust製のOxcが担当します。バンドル処理はRolldown、変換やminifyはOxcという役割分担です。

開発時と本番ビルドで異なるツールを使う構成には、変換処理やモジュールの扱いに違いが生じるという課題がありました。Rolldownへの統合は、ビルドの高速化に加え、こうした挙動の違いを減らすための変更でもあります。

ほかの選択肢を見ると、AのTurbopackはNext.jsで利用されるバンドラです。BのesbuildはViteが従来利用していたツールで、今回の統合で置き換えられる側に当たります。DのSWCもRust製ですが、Vite 8の標準バンドラではありません。「Rust製」という点だけでなく、Viteがどのツールを採用したかがポイントです。

参考:Vite 8.0 is out!、Migration from v7 | Vite

Q2

Vue 3.6で導入が予定されているVapor modeが、従来のVirtual DOMベースのレンダリングと決定的に違う点はどれでしょうか?(1つ選択)

  • A:コンパイル時に仮想DOMを経由せず直接DOMを更新するコードを生成する
  • B:既存のVirtual DOMベースのコンポーネントとは混在できず、アプリ全体をVapor専用で書き直す必要がある
  • C:SSR(サーバーサイドレンダリング)には対応していない
  • D:実行速度は従来のVirtual DOM方式より遅くなるが、バンドルサイズは小さくなる

正解・解説

正解は A:コンパイル時に仮想DOMを経由せず直接DOMを更新するコードを生成する です。

従来のVue.jsでは、コンポーネントの状態から仮想DOMを生成し、更新前後の差分を実際のDOM(Document Object Model)に反映します。Vapor modeでは、テンプレートをコンパイルする段階で、必要なDOM操作を行うコードを生成します。状態が変化した際に仮想DOMの差分計算を経由せず、対象のDOMを直接更新できる点が大きな違いです。

これにより、仮想DOMの生成や差分計算にかかる実行時の負荷を減らし、パフォーマンスの向上やバンドルサイズの削減を目指しています。Dのように、速度を犠牲にしてサイズを小さくする仕組みではありません。

Bについては、vaporInteropPluginを利用することで従来のVirtual DOMベースのコンポーネントと混在させられるため、アプリ全体を書き直す必要はありません。ただし、Vapor modeが対応するVueのAPI(Application Programming Interface)は一部であり、既存のコンポーネントをそのまますべて移行できるという意味ではありません。

また、Vue公式のVapor Roadmapでは、「SSR / Hydration」が対応済みの機能として示されています。Vapor modeでもSSRとハイドレーションを利用できるため、SSRに対応していないとするCも誤りです。

参考:Vapor Roadmap(Vue公式リポジトリ)

LINEギフトからの挑戦状!

今回は、LINEヤフーの開発現場での経験もクイズを通じてお伝えしたいと考えました。そこで、私が担当するLINEギフトの開発中に実際に起きたバグなどを題材に、「LINEギフトからの挑戦状!」として2問を作成しました。

Q3

Vue Router 4系のアプリに、StepPageでの入力を終えると完了画面(/flow/complete)へ遷移するフローがあります。入力を終えて完了画面にいるユーザーが、iOSのスワイプバックでStepPageへ戻ろうとしました。

このとき、popHistoryStack()は何回実行されるでしょうか?(1つ選択)

router.ts

router.beforeEach((to, from) => {
  if (isGoingBack(to, from)) { // スワイプバックなので true
    popHistoryStack()
  }
})

StepPage.vue

beforeRouteEnter(to, from, next) {
  if (isDone.value) { // 入力済みなので true
    popHistoryStack()
    next('/flow/complete') // 遷移前のルートはすでに /flow/complete
    return
  }
  next()
}
  • A:StepPage側の呼び出しはbeforeEachより後になるため、後から実行された方だけが有効になり、1回だけ実行される
  • B:beforeEachとStepPage側のガードが互いを意識せずそれぞれ呼ぶため、合計2回実行される
  • C:next('/flow/complete')がNAVIGATION_DUPLICATEDとして中断されるため、StepPage側で実行済みのpopHistoryStack()は取り消され、beforeEachのisGoingBackも遷移が成立しなかったものとしてfalseと評価されるので、0回になる

正解・解説

正解は B:合計2回実行される です。

2つのガードが、同じ履歴に対してそれぞれpopHistoryStack()を呼んでいることが原因です。beforeEachが戻り操作と判定して1回、StepPageのbeforeRouteEnterが入力済みと判定してもう1回実行します。

Vue Routerの主なガード・フックは、該当するものが次の順に呼ばれます。非同期コンポーネントの解決などは省略しています。

beforeRouteLeave → beforeEach → beforeRouteUpdate → beforeEnter →
beforeRouteEnter → beforeResolve → 遷移確定 → afterEach

後から動いたガードが先の処理を打ち消すことはなく、リダイレクトや遷移の中断によって実行済みの状態変更が巻き戻ることもありません。そのため、AとCは誤りです。

修正例では、戻り操作のpopをbeforeEachに任せ、StepPage側では重ねて呼ばないようにします。

beforeRouteEnter(to, from, next) {
  if (isDone.value) {
    if (!isGoingBack(to, from)) {
      popHistoryStack() // 戻り操作ではbeforeEachが実行済みなので呼ばない
    }
    next('/flow/complete')
    return
  }
  next()
}

参考:Navigation Guards | Vue Router

Q4

このコードを@vueuse/core 14.x環境で動かすと、「ボタンを5回クリックするとwindowにaddEventListenerが5本残ったまま、removeEventListenerが1本も呼ばれない」というメモリリークが発生しました。

原因として正しいものはどれでしょうか?(1つ選択)

<script setup>
import { useLocalStorage } from '@vueuse/core'

const onClick = () => {
  useLocalStorage('registered', 0).value = 1
}
</script>

<template>
  <button @click="onClick">登録する</button>
</template>
  • A:VueUseのクリーンアップはコンポーネントのonUnmountedに登録されるため、setupの実行が終わったあとの呼び出しでは登録先がないから
  • B:返ってきたrefをテンプレートから参照していないため、リアクティブな依存が生まれずクリーンアップが実行されないから
  • C:storageイベントのリスナーはwindowに直接登録されるため、コンポーネントが破棄されても解除されないから
  • D:VueUseの内部クリーンアップはeffect scope内でしか機能せず、イベントハンドラ内の呼び出しはscope外になるから

正解・解説

正解は D:effect scopeの外で呼び出しているから です。

クリックハンドラ内でuseLocalStorageを初期化すると、リスナーがコンポーネントの自動クリーンアップの対象にならないことが原因です。

useLocalStorageは内部でwindowのstorageイベントを監視します。VueUse 14.4.0では、リスナーを解除する処理を内部のウォッチャーに登録しています。setup直下で呼べば、このウォッチャーはコンポーネントのeffect scope(リアクティブな処理をまとめて管理する仕組み)に所属します。アンマウント時にウォッチャーが停止され、リスナーも解除されます。

一方、通常のクリックハンドラ内ではそのscopeが有効ではありません。そのため、クリックのたびに作られたウォッチャーとリスナーが自動で後片付けされず、5回クリックするとリスナーが5本残ります。

Cは今回の現象には当てはまりますが、原因の説明が異なります。同じwindowへの登録でも、setup直下で初期化すれば解除されます。 登録先ではなく、scopeへの紐づきがポイントです。

Aのように解除処理を直接onUnmountedに登録する仕組みでも、Bのようにテンプレートからrefを参照することが解除の条件でもありません。onMounted内で初期化した場合も、アンマウント時にリスナーが解除されます。また、自分で作ったeffectScope()内で初期化した場合も、scopeを停止するとリスナーが解除されます。つまり、「setupの実行後はすべて解除されない」というわけではありません。

修正例では、setup直下で一度だけ初期化し、クリック時には返されたrefの値だけを更新します。

<script setup>
import { useLocalStorage } from '@vueuse/core'

const registered = useLocalStorage('registered', 0)

const onClick = () => {
  registered.value = 1
}
</script>

<template>
  <button @click="onClick">登録する</button>
</template>

参考:useLocalStorage | VueUse

おわりに

本記事では、Vue Fes Japan 2026で出題した全4問のクイズの紹介と解説を行いました。

今年は、クイズを通じてLINEヤフーの開発現場も知っていただきたいと考え、企画を見直しました。昨年までは、Vue.jsの知識を問う問題が中心で、LINEヤフーならではの要素が伝わりにくいと感じていました。そこで、私が担当するLINEギフトの開発経験を題材に取り入れました。

また、難しくなりすぎないよう、各問の正解を1つ選ぶ形式に統一しました。短い時間でも気軽に参加していただけるよう、問題数も昨年より減らして全4問としています。

最後までお読みいただき、ありがとうございました。今後のイベント参加や活動についても、引き続きLINEヤフー Tech Blogで紹介していきます。