はじめに
以前、自分が書いたVueのコードで奇妙な現象を目の当たりにしました。
ブラウザバックを高速連打すると、連打した分だけデータが追加されるような現象です。
個人的に結構衝撃的で、vueの特性やテストの方法が甘かったと痛感する出来事だったので記事としてまとめようと思います。
問題となった事象
実際のコードは載せられないので、それっぽい事象が起こるサンプルを用意しました。
検索画面を想定しており
one→two→three の順番で検索し、 通信環境の良くない状態で、ブラウザバックを連打すると連打した回数分データがアイテムリストに追加されます😰
検索用コンポーネント
<template>
<div class="about">
<h1>This is an about page</h1>
<div>
<div>
<input type="text" v-model="searchQuery" />
</div>
<button @click="search">検索</button>
</div>
<list-view-vue :searchQuery="searchQuery"/>
</div>
</template>
<script>
import ListViewVue from "./ListView.vue";
export default {
name: "AboutView",
components: {
ListViewVue,
},
data() {
return {
searchQuery: "default",
items: [],
};
},
mounted() {
this.searchQuery = this.$route.query.word;
this.search();
},
methods: {
search() {
this.$router.push({ name: "about", query: { word: this.searchQuery } });
},
},
};
</script>
検索結果コンポーネント
検索によって渡されたキーワードを元に非同期処理を行い、取得した検索結果を自身が管理するアイテム配列に追加する。
<template>
<div>
<template v-for="(item, index) in items" :key="index">
<p>{{ item }}</p>
</template>
</div>
</template>
<script>
export default {
name: "ListView",
props: {
searchQuery: String,
},
data() {
return {
items: [],
};
},
mounted() {
this.search();
},
methods: {
// ちょっとだけ時間のかかる非同期処理
search() {
setTimeout(() => {
this.items.push(`${this.searchQuery}_1`);
this.items.push(`${this.searchQuery}_2`);
this.items.push(`${this.searchQuery}_3`);
}, 1000);
},
},
// urlの変更を感知して、データを再取得する
watch: {
$route() {
this.items = [];
this.search();
},
},
};
</script>
router
vue-routerを追加する際に基本的な設定を追加しただけのシンプルなファイル
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
component: AboutView,
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
原因
根本的な原因としては、
- 非同期の処理がそもそも重い
- あるいは通信環境の良くない環境で操作する
などの条件で上記のようなアプリを動かすと、非同期処理の処理遅れが発生する。その結果、 最終的に表示されるコンポーネントが今までの非同期処理の結果を一身に受けてしまう。
通信環境のいい所で開発することが多いので上記のようなバグは比較的見つかりにくいです。😭
どう対応するか
keyを活用して、不要なコンポーネントを破棄する
コンポーネントに一意の識別子(key属性)を指定し、keyが変更したことを感知して古いコンポーネントを破棄してコンポーネントを再マウントできます。
※ そもそも手動テストの視点が甘かったというものもあるので、通信状態を悪くしてシミュレートして顧客の体験に近づけた状態でテストするなどの工夫も必要です。
変更点
<template>
<div class="about">
<h1>This is an about page</h1>
<div>
<div>
<input type="text" v-model="searchQuery" />
</div>
<button @click="search">検索</button>
</div>
<!-- 子コンポーネントにkeyを指定する -->
<list-view-vue :searchQuery="searchQuery" :key='renderKey'/>
</div>
</template>
<script>
import ListViewVue from "./ListView.vue";
export default {
name: "AboutView",
components: {
ListViewVue,
},
data() {
return {
searchQuery: "default",
items: [],
};
},
mounted() {
this.searchQuery = this.$route.query.word;
this.search();
},
methods: {
search() {
this.$router.push({ name: "about", query: { word: this.searchQuery } });
// 検索実行時に確定キーを変更する
this.renderKey = this.searchQuery;
},
},
};
</script>
上記では「検索確定キー」を子コンポーネントに指定することで、検索ワードが変更されるたびに子コンポーネントが再マウントされます。よってブラウザバックを連打しても、 先に実行された非同期処理の結果を受けるはずのコンポーネントが破棄されているので、過去の皺寄せを受けることがなくなります。
再レンダリングの方法として v-if
を使用する方法もありますが、key
を指定する方がコンポーネントの依存変数が明確になり明示的です。
Chromeで通信を遅くシミュレートしてテストする
検証ツールで「Network」タブ内にある「No throttling」から擬似通信環境を選択できます。
選択肢としてデフォルトで以下が用意されています。
- Fast 3G
- Slow 3G
- Offline
任意のものを選択して、検証ツールを開いたままリロードすると通信環境をシミュレートできます。「Slow 3G」で実行するとかなり遅くなります。
この状態でアプリを動かしてみることで、意図しない見え方でWebサイトが閲覧されていないかの確認も大切だと痛感しました。
reactでも起こるのか
フロントエンドフレームワークで今最もメジャーな存在であるreact
はコンポーネントが保持するprops
や state
が変更された場合に、変更されたコンポーネントとその子コンポーネントも含めて、再レンダリングが実行されます。
そのため、 reactならば上記のようなバグは設置されにくいのかもしれません。
しかし、それはそれで、不要なレンダリングも起きやすいのでパフォーマンス的な観点で注意が必要です。
vueは不要なレンダリングが起きにくい分、コンポーネントの破棄、レンダリングを意識した方がいいかもしれません
最後に
ここまで見ていただきありがとうございますm(_ _)m。
vueのレンダリングにおける注意点を明確にできていい勉強になりました。
レンダリングパフォ、手動テストでも広い視点を持って開発していきたいと思います。