Gatsbyの<Link>を読む
The <Link> component drives a powerful performance feature called preloading.
Preloading is used to prefetch page resources so that the resources are available by the time the user navigates to the page. We use the browser’s Intersection Observer API to observe when a <Link> component enters the user viewport and then start a low-priority request for the linked page’s resources.
Then when a user moves their mouse over a link and the onMouseOver event is triggered, we upgrade the fetches to high-priority.
とりあえずIntersectionObserverとonMouseOverを使っていることが分かる
すこし後ろにはこんな事が書いてある:
Gatsby’s Link component extends the Link component from Reach Router to add useful enhancements specific to Gatsby.
最終更新は去年!(2023-01-27)
さいきんだ~
Readmeを見る限りは React 18・RSC・CJS/ESM対応が主っぽいけど
GatsbyLink という名前の、React.Componentをextendしたクラスがある(class componentsなんですね)
render() でreturnしているのは ReachRouterLink で、特筆すべきは以下:
innerRef={this.handleRef}
createIntersectionObserver を呼んでいて、viewport内に入ったら this._prefetch() の戻り値を this.abortPrefetch に代入
_prefetch() はpathのparse的なことをやって移動先を取得し、現在のpathと違うなら ___loader.enqueue() の戻り値を返す
アンダーバーみっつ
同じならundefinedを返す;ページ移動しないということだろう
this.abortPrefetch は componentWillUnmount() とかそのへんで this.abortPrefetch.abort() という形で呼ばれる
実際は abort() だけじゃなくて then() も持っている
onMouseEnter
___loader.hovering() を呼ぶ
onClick
e.preventDefault() を読んでいるんだけど、これ Reach Router使う意味ある?
propsとしてonClickは渡されていないという理由だけど、shouldNavigate とかいうやつでちょっと処理してるっぽい?まあ必要そうならまたあとで読みます
validation みたいなことをやってwindow.___navigate() を呼ぶ
___loader とは?
ブラウザのAPIかと思ったけど検索してもGatsbyのエラーに関する情報しか出てこない
だってCだったらアンダーバー2つをprefixとする識別子ってコンパイラのキーワード?じゃないですか
リポジトリ内で検索したら docs/docs/production-app.md に書いてあった packages/gatsby/cache-dir/loader.js のことらしい
publicLoader というオブジェクトがexportされており、prefetchやhoveringなどの関数が書かれているかと思いきや、これらの関数は グローバル変数 instance の関数たちを呼ぶ wrapper だった
publicLoader.enqueue: a => instance.prefetch(a) と publicLoader.hovering: a => instance.hovering(a) が大事だろう
enqueueいらんやんけ(まあ歴史的経緯だろうな)
setLoader でinstanceに代入することができる
packages/gatsby/cache-dir/production-app.js で setLoader() に ProdLoader なるインスタンスを渡してから window.___loader = publicLoader; しているのを見つけた
同ディレクトリの app.js もあるけど、どうせgatsby develop用だろ
いちおう production-app.md を読んでみた 本番環境で実行されるJSみたいな感じで良いらしい?app-[a-z0-9].js の中身というか
なるほどね → https://scrapbox.io/files/6728ad921c8facc480fd5728.png
watasuke.net/app-[a-z0-9].js で ___loader を検索したらヒットした なるほどね2
publicLoader は interface のようなもので、実態は ProdLoader ということかな
ProdLoader は loader.js に書かれているが、実際の挙動はほとんどextend元の BaseLoader に書いてある
ProdLoader が実装しているのはコンストラクタと doPrefetch() / loadPageDataJson() / loadPartialHydrationJson()
というわけでまず BaseLoader を見よう
prefetch(pagePath)
Promise とか resolve とかを持ってる defer を作り、[pagePath, defer] を this.prefetchQueued にpushしている
queued ってなんやねん
AbortController とかいうものを作って、それを {abort: abortC.abort.bind(abortC)} として返している(?)
GatsbyLink の this.abortPrefetch.abort() というのは、そういうわけです
ちなみにこのabortのハンドラ的なとこでは、以下で紹介する _processNextPrefetchBatch() を呼んだりもしている
_processNextPrefetchBatch()
ブラウザには window.requestIdleCallback というAPIがあり、ブラウザがアイドル状態になったらこれを実行してくれる
そのcallbackに、prefetchQueued から取り出してresolve する、という処理を登録してくれるのがこれ
hovering() はなんと this.loadPage() を呼ぶだけ
loadPage(rawPath) (マジで死ぬほど長い)
まず P = findPath(rawPath) (パース的な?)(実際の変数名は pagePath だけど書くのがダルいので)
this.pageDb: Map にあったら page = pageDb.get(P) して、return Promise.resolve(page) する
this.inFlightDb: Map にあったら return inFlightDb.get(P) する
inFlightPromise = Promise.all([this.loadAppData(), this.loadPageDataJson(P)]).then(...)
ここで300行(ハア?)
(export GATSBY_PARTIAL_HYDRATION=true したとき用の処理があるが、割愛)
inFlightPromise.then( inFlightDB.delete(P) ) して、inFlightDb.set(P, inFlightPromise) して、return inFlightPromise する
inFlightDb はこの関数内でしか使われていない
Promise.all().then(300行)
loadAppData()
loadPageDataJson()
ProdLoaderからsuper経由で呼び出されるので読まないと駄目
this.pageDataDb: Mapにあったら return Promise.resolve(pageDataDb.get(P)) する
returnされるのはdevelopではない時で、今回は本番環境を想定することにするので
なかったら return this.fetchPageDataJson({P}).then( e=>pageDataDb.set(P, e) ) する
ProdLoader.loadPageDataJson()
return super.loadPageDataJson().then(...) していて、then内ではHTTP HEADリクエストで404判定をしているだけっぽい
fetchPageDataJson