GatsbyJSの始め方、サイトを作成して分かったメリットとハマりどころ
ReactベースでWebサイトを構築できるためメンテナンス性が高く、事前レンダリングによりDomが完成されたHTMLファイルを生成するためSEOにも強く、更にSPAとしての性質も持つためサイト内ページ遷移をするとpushStateの機能によりページのリロードが走らず滑らかでかつ高速というのがGatsbyJSの謳い文句である。
SEOに強いSPAを作るとなるとServerSideレンダリングが必要であったが、NodeサーバとClientを綿密に連携させる必要があるため、メンテの効率も技術的難易度も高い。
一方でGruntやGulpなどのタスクランナーやWebPackと、EJSやhtmlbarsなどのテンプレートエンジンを組み合わせて使い、静的サイトを作成する方法もあるが、これだとSPAとはならないためユーザ体験としては劣る。jQueryなどを使いDOM操作のプログラミングを続けるとなると複雑な事をする時に限界もある。
このInflTechのようにRuby on Railsはどうか?Turbolinksの機能を使えばpushStateによりReloadなしのページ遷移がそれなりに手軽に実現でき、ログイン機能等も簡単に作れる。しかしやはりSPAではないのでJSで複雑なことは行いづらい、Rubyの学習、Railsのサーバを立ち上げるのも面倒である。
そんなわけで、リッチなUIで複雑な処理もこなすアプリケーション要素を持たせつつもSEOに強いサイトというのにちょうど良いアプローチが今まで無く、従来は作りにくかったのだけどGatsbyJS、NextjsをはじめとするJAMStackのフレームワークが出てきたことによって比較的容易になったわけである。
JAMStackはJavaScript、API、Markupの頭文字を示す用語であり、それってjQueryのサイトもjsとhtmlマークアップとサーバへのapiアクセスで構成されているから当てはまるのか?など疑問に思いしっくり来ないが事前レンダリングしたHTMLを持つSPAをそう呼ぶらしい。
前置きが長くなったが、gulp + ejs + jquery + sassの組み合わせで作っていた自分のサイトパソコン選び方・購入ガイドのメンテナンスを行いやすくしたい、更にページ内遷移の速度を上げて滑らかに見せたいなどの理由から思い切ってGatsbyにリプレイスしてみたのでその記録を示す。
よくあるチュートリアル通りに作ってみました、記事数ページみたいなサイトではなく、既に本格的に運営を行っているサイトであるので、それなりには参考になるはずである。
開発セットアップ
Windows10環境でnodeのv14.15.5をインストールして開発を行った。最初WSLを使用して開発を行おうと思ったのだが、WSLとVSCodeのConnectionがたまに切れて鬱陶しかったのと、CPU性能比較表等のデータ自動取得として使用しているPuppeteerがHeadlessブラウザでしか動かないためデバッグ効率が悪いなど様々な理由によりWSLは使わないことにした。
Windowsを使っているとnodeの不具合を踏みやすいとか問題は多くて嫌なのだが、幸い開発を通してWindows固有のエラーには全く引っかからなかったため、別にLinuxでなくとも問題は無さそうである。
ともあれMac/Linux/Windowsどれでも開発ができるので好きなものを選べば良いだろう。私の場合は基本Windowsで開発し、成果物はgithubのprivateリポジトリで管理し、たまにMacで開発するという感じ。
チュートリアルを進める
解説記事は多いものの、まず最初に行うべきはGatsbyの公式チュートリアルである。情報が信頼できるものでかつ説明も豊富である。GatsbyはReactベースのフレームワークなのでComponentの作り方等掘り下げる場合には本家Reactのサイトを参照しながら進めて行くと良い。
ここに書いてあることを手を動かしながら行い、分からないことを別途ググって調べていくというのが学習する順番としては一番効率が良いだろう。
技術選定
Gatsbyフレームワークの周辺技術についてはエンジニアに選択を任されているものも多い。上述した私のサイトでどのような技術選定を行ったのかについて、選定した技術とその理由を次の節からは述べる。
スタイリングはEmotionを選択
Gatsbyのcssの記述は様々な方法があり、チュートリアルでは全体のスタイルをglobal.css
で定義し、コンポーネントのcssをxxx(コンポーネント名).module.css
で記述する例が示されている。
SASSやLESSを使いCSSを管理し、Globalに一つStyleファイルを持つというアプローチが従来は多かったが、これだと常に名前衝突を意識する必要があった。SMACSSやBEMなどの名前衝突を避けて上手くCSSを管理しようという命名規則アプローチも生み出されたものの、全ての開発者がそのやり方を理解し、、となってくると結構厳しかったりするし、名前が長ったらしいので途中で書く気が失せてくる。
この点コンポーネントモジュールとしてcssを定義する方法は、CSSの有効範囲をコンポーネントに限定できるため、名前空間に意識を払わなくて良く、中規模以上のサイトでは理に叶った方法であると言える。
ただ、DOM構造とスタイリングは密接に関わりあっていることからできる限り1ファイルでまとめた方が見通しが良い場合が多い。
Reduxを触ったことのある人ならducksパターンを知っている人もいるかと思うが、関連して一緒に編集する頻度が高いファイルは別々にするのではなく、一つにまとめた方がストレス無く開発できるというのは最近のソフトウェアでは主流となっている気がするし、実際私がこのパターンで書いていてもその通りだと感じる。
そのような考えのもとCSSをJSとして記述してComponent内に取り込むCSS-in-JSをサポートするライブラリがいくつかあるため、このうちの一つであるEmotionを使うことにした。
サイトの移行のためまずは手元のCSSをglobal cssとして置いておくのはありだが、今後のメンテナンス性を考えるとEmotionやStyled-ComponentといったCSS-in-JS
ライブラリを使うのが正解だと感じる。
MDXで記事を記述
コンテンツの記述をどうしていくか?サイトの編集者は私一人なので、WordPressなどのCMSは使わないが、Wikiのように書けてかつ、ReactのComponentを手軽に埋め込めたら楽である。ということでMarkDownの記述の中にJSXを埋め込めるMDXを採用した。gatsby-plugin-mdxで簡単に導入できる。
WordPressをCMSとして使うという方法なども紹介されているが、記事にインタラクティブなコンテンツも含めて見せたいようなケースではMDXを使った方がコンテンツとしてパワフルになるだろう。
例えばパソコン選び方・購入ガイドのパソコン診断機能はQuestionsコンポーネントをMDXの記述の中にimportして埋め込んだだけである。
import Question from "components/questionLoadable"
(中略)
さっそく診断をしてみよう!パソコン選びのポイントも含めて教えてくれます。初心者でもこの結果に従ってパソコンを購入すれば失敗しないはず。
<Question {...props} />
インタラクティブなWebサイトの構築とWikiの書きやすさを兼ねそろえた優れたツールとしてMDXはおすすめできる。
WordPressなどのCMSを使うと、従来通りのブログならよいが、少し捻ったことが難しくなってしまう可能性がある。まあ頑張ればどうにでもなるとは思うが。。どのみち記事書いているのは自分だけなのでVsCodeで執筆している。
Gatsby Imageの導入
gatsby-imageは画像を扱う上で面倒な作業を自動で行ってくれる非常に優れたプラグインであり、Imgコンポーネントとして扱うことができる。使う上で便利な点としては
- Build時にWebP画像とサイズ別の画像を生成
- 画像生成時にはみ出た部分は自動でCrop
- 例えば250x200の画像を作成しようとして250x250の画像を指定すると縦50px分を自動で切り取ってくれる。
- 画像のロード前に表示領域を確保するためレイアウト崩れが起こらない
などである。今まではWebP画像を生成するために自作のNodeプログラムをかけ、画像ロード時のレイアウト崩れを起こさないために画像のアスペクト比は画像毎に指定し、というようなアプローチで運営していたので非常に楽になったと言える。
ただBuild時間が例えば300枚の画像ファイルで2分程度かかるなど、 開発効率を下げる部分も大きい。2回目以降はキャッシュが効いて速くはなるが、それでもキャッシュクリーンしたい時も多いので毎回これだけの時間ロスはストレス大である。
数万枚も画像を使うユーザー投稿型のサイトではまず使えないだろう。もっとも、そもそもの話、Gatsbyは変更の度に全Buildを行わないといけないので分割Buildがサポートされない限りはGatsbyがユーザ投稿型サイトで日の目を見ることは無いだろう。
現在Gatsby Imageは導入しているが、画像数が1000を超える程度になると、WebP画像の生成は他のプログラムで行うなどしてBuild時に画像生成をしないようにすることを考える必要があるだろう。
Image一つに付き毎回GraphQLを記述しないといけないのか?
チュートリアルをやっているとこの疑問は必ず湧くのだが、全画像ファイルをPath付きでGraphQLから取得し、その後pathがマッチした画像を表示するようにすれば毎回GraphQLを書かずに済む。Gatsby:イメージ(画像)を表示させるための作法の記事は参考になるだろう。
自サイトの場合はこの実装をベースに拡張し、画像のファイル名やディレクトリ位置から作成する画像の種類を切り分けている。下の例ではcoverディレクトリに無く、拡張子の前が_s
で終わる画像にはsmallImg用の画像生成を行い、coverディレクトリ以下の画像はeyecatch用の画像生成を行っている。
graphql`
query {
smallImages: allFile(filter: {relativePath: {regex: "/^(?!cover\/).*_s.(jpg|jpeg|png|PNG|gif)/"}}) {
edges {
node {
relativePath
childImageSharp {
fluid(maxWidth: 250, maxHeight: 200) {
...GatsbyImageSharpFluid_withWebp_noBase64
}
}
}
}
}
coverImages: allFile(filter: {relativePath: {regex: "/cover\/.*(jpg|jpeg|png|PNG|gif)/"}}) {
edges {
node {
relativePath
childImageSharp {
fluid(maxWidth: 640, maxHeight: 360) {
...GatsbyImageSharpFluid_withWebp_tracedSVG
}
}
}
}
}
また、上の例でも使用しているが画像に限らず、毎回同じGraphQLを書いているなと感じたら、少し楽になるTipsとしてGraphQL Fragmentsはおすすめできる。GraphQLの一部分をexportしておき、他のJS内のGraphQLから再利用できるようにする方法である。
Performance
Blazing Fastなどと呼ばれるが、Reactのフレームワークを初回にダウンロードする必要があるなど初期起動は実際大して速くない。特にモバイル環境だとjsのサイズがボトルネックとなりPageSpeed Insightsでも高得点は出しにくい。
ここでは主にボトルネックとなっているファイルサイズ削減のために取り組んだ方法を上げる。
遅延Load
Gatsbyのイマイチなところは、特定のページでimportしていないcomponentも全て最終的なjsファイルにバンドルしてしまうことである。これだとComponentの数を増やすほどサイズが増加してしまう。
そこで役立つプラグインがgatsby-plugin-loadable-components-ssrである。このプラグインをインストールすると、@loadable/componentを使って遅延Loadを実現できるようになる。
すなわち、通常の使用ケースではページ遷移をしてComponentが必要になって、初めてそのComponentのJSファイルがロードされるようになる。私のサイトでは例えばquestion componentをwrapしたloadableなcomponentを作成し、それをmdxから読み込んでいる。
Loadable Component作成(components/questionLoadable.js
)
import loadable from '@loadable/component'
const LoadableComponent = loadable(() => import('./question'))
export default function Component(props) {
return (
<LoadableComponent {...props}/>
)
}
MDXでImportして使用
import Question from "components/questionLoadable"
...
<Question />
jsonファイルはimportでなくgraphQLで取り込む
当サイトのCPU比較表は大量のCPUメタデータをjsonファイルで抱えている。これをComponentからimportして使うとjsonファイル分jsファイルのサイズが肥大化してしまう。
jsonを取り込んだLoadable Componentを作成しても良いが、より簡単にはjsonファイルをmdx内(もしくはpage用のjsファイル)に記述するgraphQLで取り込むことで、サイト全体の容量が肥大化するのを抑えることができる。
jsonファイルをGraphQL経由で使用できるようにするためにはgatsby-transformer-jsonプラグインを使用すればよい。
ページに必要な情報はできる限りGraphQLから取り込むことがパフォーマンス面ではおすすめである。
Preactを利用する
Preactは軽量サイズのreact互換ライブラリーである。jQueryの軽量互換ライブラリーにZeptoというものがあるが、それに似た感じである。gzip後のサイズで30Kb程度削減できるのでモバイル環境ではそれなりに大きい値である。
ちなみに私のサイトに導入したところ、ProdのBuildだと動くがdevelopでは動かなかった。デバッグはまだできていない。利用するならば早めに導入してバグを潰していければ良いだろう。React本体ほどの柔軟性は得られないのでプロジェクトによりけりにはなるだろうが。
繰り返し表現はPureComponentを有効活用する
ファイルサイズ以外の話も幾つか話そう。GatsbyではなくReactの実装の話となるが、setStateにより一部でも状態が変わるとComponentの全体が再描画されてしまう。しかし全てを再描画するのはそれなりにコストがかかり、ユーザー体感的にもボタンを押したときの動作がもっさりと感じてしまう。
この再描画を防ぐ方法が、再描画不要のDOMをPureComponentとして切り出すことである。PureComponentは親Componentから渡されたprops及び自分自身のstateを前回のものと比較(Shallow Compare)し、変わっていなければ再Renderを走らせない仕組みである。
例えば前述のCPU比較表ではリストのヘッダー部分をクリックすると昇順または降順のソートが行われるが、この時テーブルの行の中身自体は変更する必要が無い。よって1行を表示するPureComponentを作成しておけば、中身自体の再描画を防ぐことができる。
class TableRow extends React.PureComponent {
render() {
const { cpu, fieldArray } = this.props;
return (
<tr>
<td>
...
</tr>
);
}
}
render時間を次のようにしてコマメに計測して改善していこう。WebInspectorで計測時間が表示される。
componentDidUpdate() {
console.timeEnd('render');
}
render() {
console.time('render');
先の比較表の例では400行のrender時間が35ms程度から16ms程度にまで短縮できている。ユーザインプットの反応が遅いと思ったら再描画を見直してみよう。
DeployはApacheで行った
NetlifyでGithubと連携してpushしたら自動でdeployされる、みたいな構成がもてはやされている感があるが、Netlifyは無料プランにおいてCDNノードを東京に持っていない、帯域幅制限しているとかでネットワークアクセスが遅いらしい。使ってみたいというエンジニア要求には良いが、ビジネスとしては使えない。
ということで普通にさくらVPSの2Core CPUを借りてApacheでDeployを行った。普通にBuild成果物をDocumentRootのトップに置けば動かすことができる。下記の記事等を参考にしてHTTPS化、http2対応をしておこう。
http2は大量のHTTPリクエストを処理するのには適したプロトコルであるため、prefetchを多く行うGatsbyとは相性が良いのではないかと思い一応入れておいた方が良いと思われる。(まあ体感速度向上とまでいかないけれど)
あと、Performanceに大きく影響するのでgzipでのコンテンツ圧縮は必ず入れておこう。下記のような感じ。
AddOutputFilterByType DEFLATE text/html text/css application/javascript image/svg+xml application/json
AWS lightSailでBuildできず、さくらVPSへ切り替え
レンタルサーバの話を補足しておくと、最初AWS LightSailの5ドルプランでホストしようとしたが、BuildしたらCPU能力が低すぎるのか止まってしまった。LightSailはPuppeteerを使いWebサイトのスクレイピングを試みた時も動かなかったため、ある程度馬力のいるタスクには向かないだろう。
サーバ上でBuildするなら時間もかかるので最低2コアはいると考えておけば良いと思う。
ユーザディレクトリでは動かず
テスト的に動かそうと、ユーザディレクトリ上にBuild成果物をおいて試してみたがうまく動かなかった。ルートのパスにおいてやらない駄目みたいである。簡単な動作確認としてはgatsby serveのHオプションを指定して
$ gatsby serve -H 0.0.0.0
でサーバ起動を行うことである。Hオプションは同一デバイス外からアクセスを行う時に指定するオプションであり、VPSでホストしたサイトを自分のWebブラウザから見る場合もこの同一デバイス外という制約に当てはまる。サーバ上で動作確認をする時には指定しておこう。
Adsenseはプラグインとreact-adsenseパッケージを組み合わせて導入
広告がうざいという人も多いが、適切な数の広告はしっかり運営を行っているサイトとみなされ、かえってそのサイトの信頼度を上げている感もある。そんなわけでAdsenseをGatsbyサイトにも導入したいが、ページの切り替わりにリロードが走らないサイトに対して、URLが変わるたびにAdsenseを更新するのは実は結構難しい。
GatsbyサイトにGoogleAdSenseを導入するという記事は自分でComponentを作成しているが、広告を表示するために
- 広告を出したい場所にIns要素を記述する
- Ins要素がRenderされたことをadsenseに伝える
という2つの事を行わなければならない。これを真似してComponentを作成してみたところ、Backで戻った時に意図せぬタイミングでRenderされるなど広告がうまく表示できそうになかった。
というわけで試行錯誤し最終的には
- html.jsをオーバーライドしてbaseとなるjsファイル(下記)を記述
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
html.js
に記述しておくことでページ遷移枚のadsense scriptのロードを回避できてパフォーマンス的にも良いはずである。
- react-adsenseを使用。
中身をじっくり見たわけではないが、先のIns要素の描画をadsenseに伝える処理などをエラーハンドリングをしっかりして書いてくれているのだと思われる。このライブラリを利用したところエラー無く表示することができた。
Prod Deploy完了まで実際の広告表示を試せない
注意点として、localhostで開発していたり、Adsense申請したホスト名以外のアドレスでホストされると広告取得が403エラーとなり広告の表示ができない。
ただ正しく設定されている場合は、Adsense表示枠の高さと幅がAdsenseScriptの実行時に確保されるので保証はできないが目安とはなるだろう。
Google Analyticsはプラグインでさくっとできる
gatsby-plugin-google-analyticを入れて、こんな感じで記述すればよい。計測していないがdeferにしておくとパフォーマンスが上がると思ったのでtrueにしておいた。
{
resolve: `gatsby-plugin-google-analytics`,
options: {
// The property ID; the tracking code won't be generated without it
trackingId: "UA-XXXXXXX-X",
// Defers execution of google analytics script after page load
defer: true,
},
},
このプログラムはproduction buildでないと動作しないため、動作確認では注意が必要。
実際ハマったところのメモ
チュートリアルだけでは躓きそうな、というより実際に躓いたところをメモしていく
production buildではpathが渡ってこない
MDXを使いdefaultのレイアウトとしてLayoutを指定すると、propsが渡ってくるのだが、production buildをした時にpropsのpathプロパティが無くてエラーとなった。Build時にはページ遷移などしないのだから同様にBuild時にはwindow.location
にアクセスできないなどの制約がある。
解決策としては、MDXのpathに
---
path: '/cpu/'
---
などと書いておけば、props.pageContext.frontmatter.path
で取り出すことができる。(MDXプラグインを使っている場合)
develop buildとproduction buildで動作が異なる事が多々ある
特にレイアウト、gatsby-browser.js
のwrapPageElement
をサイト共通のレイアウト適用のために使用するとCumulative Layout Shift、すなわちページ表示をした瞬間にレイアウトが崩れたように見える現象が発生する。
const React = require("react")
const Layout = require("./src/components/layout").default
export const wrapPageElement = ({ element, props }) => {
return (
<>
<Layout {...props}>{element}</Layout>
</>
)
}
gatsby-browser.jsはサイト共通の処理を書く場所として適してはいるが、Build時にRenderされず、index.htmlの中に含まれてこないため、ページ表示時にJavaScriptが動作してRenderされることになる。この結果index.html内のrender済レイアウトが更新され、表示がカクついて見えてしまうのである。
試していないが同じ記述をgatsby-ssr.js
でも書いてやると恐らく解決するだろう。
またスタイルの当たり方がdeveopとproductionで異なる不具合はGatsbyのgithubでも多々報告されているが対処法は人それぞれな模様。私も一回遭遇したが、修正方法は忘れてしまった。
Inconsistent CSS Styling Differs Between gatsby develop and build - Why?
gatsby-plugin-mdxのサブプラグインを外部にも記述しなければならない
gatsby-pugin-mdx
はgatsbyRemarkPluginsをオプションとして指定できるが、プラグインをオプションに含めるだけだと上手く機能しないことがあり、結局
{
{
resolve: `gatsby-transformer-remark`,
options: {
plugins: [
{
resolve: `gatsby-remark-autolink-headers`,
},
{
resolve: "gatsby-remark-external-links",
options: {
rel: "nofollow noopener"
}
}
],
},
},
{
resolve: 'gatsby-plugin-mdx',
options: {
defaultLayouts: {
default: require.resolve("./src/components/layout.js"),
},
gatsbyRemarkPlugins: [
{
resolve: `gatsby-remark-autolink-headers`,
のように2度同じ記述をしなければ上手く動かなかった。サブプラグインとプラグインでは動作の方法が若干異なり、サブプラグインの使われ方を十分に想定したプラグインでなければ動かないとgithub内で説明している人がいた。
ページ内リンクの遷移は@reach/routerのnavigate関数を使う
<a href="#"></a>
とかで遷移するとどこか不自然な動きとなってしまう。
他お世話になったプラグイン等
- gatsby-plugin-sitemap サイトマップの自動追加。カスタマイズ可能で自由度が高い。
- gatsby-plugin-react-helmet SEO対策やTwitterなどのメタデータ設定に必要。
- gatsby-plugin-manifest PWA対応に必要。個人的にはモバイルで見た時のブラウザの色を変えたかったので使用。
- gatsby-plugin-root-import Rootからのpathでimportができるプラグイン
../../../components/
のような相対パスにならずに済む。
感想・まとめ
目下開発中でいろいろハマりどころは多いフレームワークであるが、一度作ってしまえばその後の機能拡張は各段に楽になると感じられた。パフォーマンス的にも優れているので、Reactをある程度使える人でかつ、大量にページがあるユーザ投稿型コンテンツ以外のリッチなサイトを構築するならば使ってみても良いだろう。