2021-02-25

turbolinksで実現したかったことって、こういうことだったのか

Turbo: The speed of a single-page web application without having to write any JavaScript.

Stimulus ほどあれこれ考察はしていないけど、Turbo を軽く触ってみて結構よさげという印象を受けた。Google Analytics などの 3rd party のツールとの組み合わせの部分には注意が必要になるが、特に初期実装ではかなりの威力になりそうだ。

※ 今回は Turbo Stream は試していない。あくまで Turbo Drive と Turbo Frame の話。

Stimulusはturbolinksに欠けていたもの

まず あーありがち - Stimulus、悪くない の方を読んでほしいんだけど、Turbo は turbolinks が洗練されて戻ってきたものであることから始めると導入しやすいと思う。

細かく引用はしないが、turbolinks 登場時、最も困ったのは当時全盛と言ってよい jQuery を利用したコードと極めて相性が悪かった点であった。なんなら Rails 標準の JavaScript のライブラリが jQuery なのにどうなってんだこれは、という話である。

どういうことかというと、turbolinks を有効にしてしまうと jQuery を利用した以下のような典型的なコード

jQuery(function() {
  ..
})

が、初回のページロード時には動くが、ページ遷移やブラウザの「戻る」ボタンを押した際には動作しない、という問題があったのだ。というか今もこれはある。jQuery をやめて

document.AddEventListener('DOMContentLoaded', () => {
  ..
})

にしても問題は解決しない。なにせ turblinks も Turbo の Turbo Drive も

ページ遷移自体はせず、DOMの一部とHistoryへの変更であたかも遷移したフリをする

ものだからだ。

turbolinks や Turbo のために監視するイベントを増やしたりする方法もあるかもしれないが、それなら今なら潔く VDOM 系のツールで SPA を組む方が賛同を得られやすいだろう。ただし開発コストは大きく膨らむが。

しかし、Stimulus を使うことでこれは解消される。Stimulus ではこの部分は Application.start() の中に隠蔽されていて、DOM がゼロベースで構築完了したかバックグラウンドで必要な HTML を取得して一部の書き換えを行ったかに関わらず Controller の connect() メソッドは呼ばれる。

つまり、Stimulus を使うことでページの読み込みを監視する処理をそもそも自分で書く必要がなくなり、それによって Turbo を利用しているかいないかに関わらず安定して同じように動作するのだ。

これは非常に助かる。これまで turbolinks を off にするのは割とよくある話だったと思うが、フロントエンドエンジニアを多く確保しなくても Turbo と Stimulus を組み合わせることで、よくある多くのパターンの動的な DOM 構築に対応でき、明らかに体感速度は向上する。

実際にブラウザの Developer Tools で見てみると HTML を取得するたった一つの connection で、JavaScript も Stylesheet も読み込み直すことなくシュッと遷移するのは実に快感だ。

方法は簡単。例えば Webpack や Vite などの bundle ツールを利用していたら import するだけである。それだけで有効になり、ページ遷移は爆速になる。具体的には

import * as Turbo from '@hotwired/turbo'

かな。これだけでよい。使っていなくても大丈夫。CDN から script タグで読み込むだけで動く。

Turbo Frameはiframeを使わないiframeのような何か

これも非常によくあると思うのだが、HTMLの中の一部のコンテンツを遅延ロードしたいという場合、以下のように書くと実現できる。

読み込む側 /importer

<turbo-frame id="export" src="/exporter">
</turbo-frame>

読み込まれる側 /exporter

<turbo-frame id="export">
  ..
</turbo-frame>

これで importer 側の <turbo-frame> の中身が exporter 側の <turbo-frame> の中身に挿し変わる。

id でコンテンツを紐付けて対応する DOM を入れ替えてしまうという方法である。

同じことを JavaScript で実現する側として正解を挙げると、従来なら

  • サーバサイドで JSON を出力
  • クライアントサイドで JSON を取得
  • クライアントサイドで(サーバサイドとは異なる記法で)DOM を構築

という3ステップを要する。

しかし Turbo Frame を利用すれば全部端折ってサーバサイドで普通に HTML を書いて別な entry point を与えればよい。bot 避けに meta でも書いておけば変なお漏らしもないだろう。これだけで済む。

もちろん、そもそもコンテンツをどのように見せるかは import する側の責務なのでは? という指摘はあると思う。自分もそう思う。でもじゃあそのためにフロントエンドのツールでゼロから書きますか、というと「なんだかなぁ」という気がしないだろうか。DOM の書き換えだってせいぜい最初の一回しか起きないのに VirtualDOM とか必要ですか?という気持ちになるし、今書いているバックエンドのコードから完全に頭を切り変えてフロントエンドの海に潜らないといけない。

Turbo を使うとそこはずっとサーバというか HTML のままで実現できるので、スイッチングコストが発生しない。しかも Turbo Drive でのページ遷移と組み合わせると、遅延読み込みしたかどうかも人間にはよく分からない。

まとめ - Stimulus + Turboはよいぞ

以上のような形で

  • サーバ側の routing (あるいは単なる HTML のパス)で
  • サーバ側の HTML の生成で
  • JavaScript をいちいち書かずに

ユーザーの体験を大きく向上させるのが Turbo の狙いだ。

そしてより細かい部分や 3rd party のツールと組み合わせる際には Turbo 固有の event を利用すればよい。

Hotwire という名前や DHH という名前で大袈裟に構えることはない。なんてことのない、だが確実によく利く勘所を押さえたツールと言えそうだ。

フロントエンドのツールの理解、VirtualDOM や reactive という考え方はそれはそれで必要になるし、例えばタイマーのようなより細粒度、高頻度の更新が必要なものは JavaScript で閉じた方がよいと思うが、ほとんどサーバ側が正解の情報(HTMLなど)を持っているのに JavaScript から遠回りして DOM に反映しているケースも実は多い。

そういう場合に .js.erb のように無理やり JavaScript に情報を渡すのではなく、もうそのまま HTML を渡せばいいじゃんという考え方は一見大胆だし、細かくベンチマークを測るとそこまで速くもないかもしれないけど、そこそこ十分に速くて開発コストが大きく抑えられるのもまた事実。混ぜて使う際にはやや注意が必要かもしれないが、少なくとも初手でコストを掛けずにユーザーの体験をよくするためには大きく貢献してくれそうだ。

※ テストが難しそうというかエラーハンドリング周りの情報が全然ない1状態なのは確かに気になるので、そこは追試が必要。あと HTML を「直接」「普通に」取得するのでサーバサイドや昨今のフロントエンドを書く際に忘れていたキャッシュ周りに気をつけないとハマる可能性もある。あと地味にレビューが難しい。Stimulus も Turbo も生々しく自分で実装する量がグッと減るので、テストコードも恐らくかなり少なくなる代わりに、HTML 側の記述ミス防止は恐らく目視チェックになりやすいので、何か工夫があった方がよさそう。

  1. 普通に console にエラーが出るんだけど、その情報が十分に出揃ってない。 

About

例によって個人のなんちゃらです

Recent Posts

Categories

Tool 日々 Web Biz Net Apple MS ことば News Unix howto Food PHP Movie Edu Community Book Security Text TV Perl Ruby Music Pdoc 生き方 RDoc ViewCVS CVS Rsync Disk Mail FreeBSD Cygwin PDF Photo Zebedee Debian OSX Comic Cron Sysadmin Font Analog iCal Sunbird DNS Linux Wiki Emacs Thunderbird Sitecopy Terminal Drawing tDiary AppleScript Life Money Omni PukiWiki Xen XREA Zsh Screen CASL Firefox Fink zsh haXe Ecmascript PATH_INFO SQLite PEAR Lighttpd FastCGI Subversion au prototype.js jsUnit Apache Trac Template Java Rhino Mochikit Feed Bloglines CSS del.icio.us SBS qwikWeb gettext Ajax JSDoc Rails HTML CHM EPWING NDTP EB IE CLI ck ThinkPad Toy WSH RFC readline rlwrap ImageMagick epeg Frenzy sysprep Ubuntu MeCab DTP ERD DBMS eclipse Eclipse Awk RD Diigo XAMPP RubyGems PHPDoc iCab DOM YAML Camino Geekmonkey w3m Scheme Gauche Lisp JSAN Google VMware DSL SLAX Safari Markdown Textile IRC Jabber Fastladder MacPorts LLSpirit CPAN Mozilla Twitter OpenFL Rswatch ITS NTP GUI Pragger Yapra XML Mobile Git Study JSON VirtualBox Samba Pear Growl Mercurial Rack Capistrano Rake Win RSS Mechanize Sitemaps Android JavaScript Python RTM OOo iPod Yahoo Unicode Github iTunes God SBM friendfeed Friendfeed HokuUn Sinatra TDD Test Project Evernote iPad Geohash Location Map Search Simplenote Image WebKit RSpec Phone CSV WiMAX USB Chrome RubyKaigi RubyKaigi2011 Space CoffeeScript Nokogiri Hpricot Rubygems jQuery Node GTD CI UX Design VCS Kanazawa.rb Kindle Amazon Agile Vagrant Chef Windows Composer Dotenv PaaS Itamae SaaS Docker Swagger Grape WebAPI Microservices OmniAuth HTTP 分析基盤 CDN Terraform IaaS HCL Webpack Vue.js BigQuery Middleman CMS AWS PNG Laravel Selenium OAuth OpenAPI GitHub UML GCP TypeScript SQL Hanami Document SVG AsciiDoc Pandoc DocBook Develop Jekyll macOS Node.js Vite Heroku Transformer AI Data Cloud Wasm