generated at
「SPAのタブ永遠に開きっぱなし問題」を更新ボタンを設置せず解決した



こんにちは。強いUIはボタンを捨てるをスローガンにScrapboxを開発しています。shokaiですshokai
Helpfeel Advent Calendar 2022の5日目の記事です
昨日はHelpfeelエンジニアのyadoさんでした
ヨコハマハウスフラペチーノがエンジニア採用の役にたった?みたいで良かったです


<a> タグの挙動を工夫する事で、Scrapboxからみたいなボタンをなくしました
更新ボタンの役割は2つ
更新がある事を教える
押すとアプリが更新される
Scrapboxも昔こういうメニューがあった
今はもう無い


では解説ですshokai


SPAのタブ永遠に開きっぱなし問題とは?

SPAとstaticなwebサイトの違い
staticなwebサイトでリンクをクリックすると
最新のassets(HTML/JavaScript/CSS/画像)を取得して、画面遷移する
SPAはpushStateで画面遷移するので
最初に読み込んだassetsを使いまわしつつ
主に本文のデータだけをAjaxで取得し、画面表示を更新する
ユーザーによっては、昔の古いassetsをずっと使い続ける事になる
ブラウザをリロードするか、ページを新規ウィンドウで開き直すまで

webブラウザが古いassets(HTML/JavaScript/CSS)を使い続けると
サーバー側のAPIが更新された場合に、うまく動かない可能性がある
より良い挙動に修正されたバージョンがあるのに使ってもらえない事もある
「そのbugはリロードしたら直るのでリロードしてください」というサポート対応が、一定数あったりする

特にScrapboxでは発生しやすい
メモを書くアプリは開きっぱなしになりがち
SPAである
ブラウザを閉じたりリロードしたりせずに、何日もscrapboxを使い続けるユーザーが多い
ブラウザのタブ開き過ぎで、200個ぐらい開いてる人もいる
それら1つずつをリロードしてくれる日は永遠に来ない
最長で6ヶ月前のassetsが使い続けられていた
2021年に判明した
Socket.IOのメジャーバージョン更新を行った時
互換性の無くなった古いclientで接続→サーバーから拒否→再接続→拒否→再接続…
が繰り返される事、6ヶ月間
淡々と記録される接続エラー


ちなみに、ScrapboxがSPAとしてどんな事をやっているかは
スマホのネイティブアプリみたいになっている
サーバーから取得するのは(ほぼ)コンテンツのテキストデータだけ
assets(HTML/JavaScript/CSS/画像)はアプリの様にインストールされ、バックグラウンドで更新される


新しいassetsが準備できている時だけpushStateしない様にした

そうして生まれたのがこの機能です

上のGIFは、新しいアプリケーションコードをサーバーにデプロイした直後の様子をローカル開発環境で再現したものです
サーバー側で npm run build:assets-json を実行してから
このコマンドが、新しいclient jsのビルドに相当する
3回の画面遷移を行っている
1回目と3回目の <a> タグクリック
pushStateである
SPAとしての画面遷移
エディタの中だけが再描画されている
navbarや右側のメニューは再描画されていない
2回目の <a> タグクリック
画面全体がリロードされている
SPAではないstaticなwebサイトの画面遷移
assets(HTML/JavaScript/CSS)が更新される
これをfull document loadと呼ぶ
pushStateと対比した場合の呼称として、社内では仮にこう呼んでいますshokai

何がおきているのか?
1回目の <a> タグクリックによる通信で
ServiceWorkerが新しいassets cacheを検知し、3秒後にダウンロードを開始する
2秒ほどでダウンロードが完了
以後、 <a> タグの挙動が切り替わる
リンククリック後にpushStateしなくなり、full document loadするモードになる
2回目の <a> タグクリック
full document loadが実行され、最新のassetsに切り替わる
assetsは既にCacheStorageにダウンロードされているので、十分速い
どれぐらい速いかというと、この挙動に気づいて「たまにpushStateしない事あるよね」と言及しているユーザーが全く見当たらないぐらい速いshokai


ここまでの道のり
2019年ごろには思いついていた
しかし、すぐ実装するのは無理だった
準備として、色々な改善が行われた
サーバーからの読み込みを減らして初回レンダリング速度を上げる
assets cacheがかなり高い頻度で更新されるようになった
full document loadによる画面チラつきを低減させた
予備テスト
page & pagelist画面からproject設定画面への画面遷移にpushStateを使わないようにしてみた
ほとんど気にならなかったshokai
たぶんshokaibalar以外誰も気づいてない
同じ事をリンク記法関連ページリストで移動した時もやればいい、と決心がついた



なぜユーザーによる画面遷移(Aタグクリック)をトリガーとするのか?

みたいなボタンは設置しない
目立つ更新ボタンを置くと「今テキスト書いてるんだから邪魔するな」という気分になる
かといって、更新を促すボタンは目立たなければ意味が無い
ボタンを増やすのは安直で簡単。強いUIはボタンを捨てる
Scrapboxは年間500回以上リリースされる
ユーザーは毎日1〜4回「更新があります」を見る事になる
しかも、たくさんブラウザタブを開いている人は、その数だけリロードボタンを押して回らなければならない
更新地獄である
更新がありますアラートはリリース頻度が低いネイティブアプリケーションの文化だ
例えば一週間以上古い時のみ「更新があります」を表示する事も可能だが
どうせ、重要なセキュリティアップデートがある時は更新を強く主張したくなる
各リリースに「重要かどうか」という、新たなメタ情報が必要となる
運用がどんどん複雑になる
heroku rollbackとの相性も悪い
重要フラグをつけたリリースにbugがあった場合
rollbackするとサーバーは巻き戻るのに、クライアントは巻き戻らないかもしれない
ユーザー
開発者
サービス運用者
攻撃者
誰かの都合・対策を優先すると、他がそのぶんの面倒をかぶってしまう
オンプレ版Scrapboxのサービス運用者にも優しい仕様にしたい
不具合等でオンプレ版docker imageを古いバージョンに戻す事がある
docker imageのtagを切り替えて、コンテナを再起動するだけで古いassetsが行き渡るようにした

しばらく、古いclient jsで動いているwindowを自動リロードするというアイディアも検討していたが
やはり自動リロードはタイミングが難しい
ユーザーの操作を邪魔してしまう
input formに入力中かもしれない
未送信のデータが残っているかもしれない
ちゃんと動作してるか?の検証も難しい
長時間ブラウザを放置する、等


最終的にこうなった

動作フロー
全てのHTTP APIのx-assets-version headerをチェック
更新を検知し、3秒後にassets cacheの更新を開始
CacheStorageが更新される
ユーザーが <a> タグをクリック
以下の条件を全て満たす時
今ロードされているHTML documentとassets cacheのバージョンが一致しない
未保存の編集データが無い


未保存の編集データがある場合の様子
右下にspinnerがグルグル回っているのが、未保存の状態
新しいassets cacheが準備できていてもfull document loadせず、pushStateを行う
こうしないと編集データを失ってしまう事がある

結果
ブラウザのリロードボタンを押さなくても、最新のassets(HTML/JavaScript/CSS)に更新されるようになった
開発者視点
古いclientの存在を考慮せず、サーバー側のAPIをガンガン更新してもわりと大丈夫になった
ユーザー視点
普通に使っていれば、画面遷移のタイミングでリロード相当の事を適宜やってくれて便利
「そのbugはリロードしたら直るのでリロードしてください」というサポート対応がゼロになった
「なんか突然リロードされてデータが消えました」という苦情も無い
full document loadによってSPAが更新されている事に気づいている人も、たぶんいない
あまりにも自然に2種類の画面遷移が使い分けられている為


というような感じで、 株式会社Helpfeelでは、シンプルなUIを実現するため「ボタンをもっとわかりやすく、ラベルも工夫して……いや、アルゴリズムを工夫すればボタンぜんぶ消滅させられるのでは!?」という根源的な思考ができるエンジニアを募集しています

明日はテクニカルライターのmiyabaraさんです