| @技術/プログラミング

react-select-before-after.jpg

一個前の記事のキャプチャにあるように、従来テキストリンクだった年とカテゴリーの選択をセレクトボックスにした。 git log -S したところ去年( 2019 年)の 11 月頃に変更を行ったみたいだ。もうそろそろ 2020 年になろうとしていて、 2005 年からやっていて年の数が 16 個になろうとしていてさすがに多すぎると思ったので整理のためにセレクトボックス化してコンパクトにした。

利用したのは React Select というパッケージで、色々カスタマイズできるみたいだけど面倒だったので素のまま使ってる。

Archives ページのリファクタリング のときのように、子コンポーネントのイベントをトリガーに親コンポーネントの setState() を呼び出すような作りになっている。作るときは結構難儀したけどおかげでスマートフォンで見たときも年やカテゴリーだけでファーストビューが埋まるということがなくなった。

react-select-smartphone-view.jpg

現在のコードをまるっと貼り付けるとこんな感じ↓。

子コンポーネント(年のセレクトボックス)

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import Select from 'react-select'
import PropTypes from 'prop-types'
import { withRouter } from 'react-router-dom'

import history from './history'

class YearSelect extends Component {
  constructor(props) {
    super(props)
    this.state = {
      data: [],
      selectedOption: null
    }
    this.handleChange = this.handleChange.bind(this)
  }

  static propTypes = {
    match: PropTypes.object.isRequired,
    location: PropTypes.object.isRequired,
    history: PropTypes.object.isRequired
  }

  async loadYearSelectFromServer() {
    const request = await fetch('/archives/years.json')
    const response = await request.json()
    this.setState({ data: response })
  }

  componentDidMount() {
    this.loadYearSelectFromServer()
  }

  handleChange(selectedOption) {
    const year = selectedOption ? selectedOption.value : null
    if (year) {
      this.props.history.push(`/archives/${year}`)
    } else {
      this.props.history.push("/archives")
    }
    this.setState(
      { selectedOption },
      () => { this.props.update(year) }
    )
  }

  render() {
    const options = this.state.data.map(year => {
      return { value: year, label: year }
    })
    return (
      <div className="year-list">
        <Select
          value={this.state.selectedOption}
          onChange={this.handleChange}
          options={options}
          placeholder="Year"
          isClearable
        />
      </div>
    )
  }
}

const YearList = withRouter(YearSelect)

export default YearList

親コンポーネント( App.js )

import React, { Component } from 'react'
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom'

import YearList from './YearList'
import CategoryList from './CategoryList'
import Archives from './Archives'

class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      category: null,
      year: null,
      length: 0
    }
    this.updateCategory = this.updateCategory.bind(this)
    this.updateYear = this.updateYear.bind(this)
    this.setLength = this.setLength.bind(this)
  }

  updateCategory(category) {
    this.setState({ category })
  }

  updateYear(year) {
    this.setState({ year })
  }

  setLength(length) {
    this.setState({ length })
  }

  render() {
    return(
      <Router history={history}>
        <div className="archive-filter">
          <YearList update={this.updateYear} />
          <CategoryList update={this.updateCategory} activeCategory={this.state.category} />
          <div className="entry-length"><p>{this.state.length} entries</p></div>
        </div>
        <Switch>
          <Route
            exact path="/archives"
            render={(props) =>
              <Archives
                category={this.state.category}
                setLength={this.setLength}
                {...props}
              />
            }
          />
          <Route
            path="/archives/:year(\d{4})"
            render={(props) =>
              <Archives
                category={this.state.category}
                setLength={this.setLength}
                year={this.state.year}
                {...props}
              />
            }
          />
        </Switch>
      </Router>
    )
  }
}

export default App

| @Mac/iPhone

Eagle

S3 のバケットを事故で空にしてしまってとても悲しかったので、写真以外の画像もきちんと管理してみることにした。

これまで、仕事やブログ用に作った図表みたいなやつは作ったあと JPEG とか PNG に書き出してドキュメントに書き出した後は適当にデスクトップに置いてあって、ある程度時間が経ったら定期的に削除するような運用にしていた。図表自体は Pixelmator や OmniGraffle で作っているが、ちょっと作ったやつは元ファイルごと消してしまっていた。しかしそれでは今回のようにミスって S3 を飛ばしてしまったときにリカバリできなくなる。

というわけで Pixelmator や OmniGraffle で作ったオリジナルファイルを残していこうと思ったのだけど、これらはプレビューできないし、 Mac の中で仕事のプロジェクトのフォルダの中にあったり、 iCloud の Pixelmator のフォルダの中にあったりデスクトップにあったりで非常にごちゃごちゃしており、 Finder だけで管理するのは無理っぽいなと思った。写真以外の画像ファイルを管理できる何かが必要だと思って調べてみた。

この手のやつは Little Snapper というのを使っていたが、途中から名前が Ember に変わって Mac OS のアップデートについて行けずディスコンになった。 Little Snappter / Ember は結構気に入って使っていて、画面のキャプチャを撮って注釈の文字を入れたりする用途に使っていたが、本来はこれらはデザイナー用の画像管理ソフトだと思う。

色々調べていて Eagle というソフトが存在することを知った。 Eagle はなかなかよくて、 JPEG や PNG 、 Adobe の Photoshop 、 Illustrator に加え Pixelmator や OmniGraffle に対応している。デザイン系だけでなく各所にファイルが散らばりがちな MindNode (マインドマップアプリ)や Numbers 、 PDF 、 Keynote 、 Pages 、 MS Excel 、 MS Word などドキュメント系のファイルにも対応している。

要するにパワーアップした Finder という感じなんだけど、ファイルの中身をプレビューできて一括でタグ付けしたりフォルダ管理できるのが便利。 いわゆる「ファイルの壁」問題が解決できる。デスクトップや iCloud Drive に散らばってるやつをとりあえず一旦突っ込んで整理してみることにした。

UI は日本語翻訳があるのだけどあまり出来がよくなく、スクリーンショットを「スクショ」と略していたり、全体的に翻訳が調子こいてる若者風なのが気持ち悪い…。いまは言語を英語にして使ってみてる。日本語文字列の検索精度がいまいちなところも気になる。 年賀状 で検索しても 年賀状 という文字列を含むファイルがヒットせず、かわりに 年賀 で検索するとヒットしたりする。日本語周りに課題が多そうだ。そのほか、 Windows 版も存在するせいか、通知の UI が Notification Center の標準 UI とかけ離れているところもちょっと気持ち悪い。

同系統の Pixave というのを以前「できる Mac OS X Advent Calendar 2015」で知って試していたが、当時の自分は Little Snapper / Ember の代替となるキャプチャに注釈を入れられる機能を求めていたので用途に合わないと感じた。いまもう一度見てみて試してみたが、 Eagle 同様にデザインデータだけでなく様々なファイルを扱えて、いまの自分の用途には適合してそうだった。惜しむらくはあまり活発に開発されていないようだった( MindNode を取り込めるとウェブサイトに書いてあるが、最新バージョンの MindNode には対応していなかった)。Pixave は App Store の賞を受賞しているだけあって見た目がめっちゃおしゃれで Apple の標準 UI に準拠しており、使い心地は Eagle よりも Pixave の方がよい。もうちょい活発にメンテナンスしてもらえたらうれしい。

しばらく併用してどっちを使うか決めたい。

※ Pixave は韓国系の人が一人で作ってるみたい。以前好きで作っていた The Hit List ( ToDo 管理ソフト)も韓国系の人が作ってた。在米コリアンは優れたデザイナーが多いのだろうか。 The Hit List はめっちゃ UI が革新的だったのに更新が滞ってしまった。 Karelia という会社に買われたあとも更新が滞っている。 The Hit List のようにならないでほしい。

| @技術/プログラミング

外出自粛かつ自分自身にコロ助疑惑があったのでずっと部屋に閉じこもってサイトのデザインをいじってた。

2020-04-07 デザインアップデート.jpg

上がこれまでで、左下が今後、右下が画像がワイドな場合のバージョン。これまで横幅 1280px 想定にして画像も横幅 1280px のサイズで表示するようにしていた。

ただしこれだと写真は見やすくても文字が読みにくい。人間の目は 1280px 繰り返し左右に移動させるのには適していないようだった( N=1 )。

そういうわけでまたまた cho45 さんのブログのレイアウトをパクって、文章は横幅短めに、写真はでかいサイズで表示するようにした。

文章部分の幅を 800px にして、画像を読み込んだときに一定の条件にマッチしたら写真の横幅を 1280px で表示するようにした。 margin-left: -250px; してるのがミソ。横幅 800px だと大分文章は読みやすいし、でかい写真は大きく見えて便利。

@media screen and (min-width: 1422px) {
  #content #main article .body > p {
    img[class~="large"] {
      width: $content-max-width;
      max-width: $content-max-width;
      margin-left: -250px;
    }
  }
}

JS はこんな感じのコードを書いた。

const checkImageSize = (target) => {
  if (typeof target === 'undefined') {
    return;
  }
  const width = target.naturalWidth;
  const height = target.naturalHeight;
  const isPhoto = RegExp('(lh3\.googleusercontent\.com|\.jpe?g$)').test(target.src);
  if (width > 1279 && width > height && isPhoto) {
    target.classList.add('large');
  }
}

const lazyImageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const lazyImage = entry.target;
      lazyImage.src = lazyImage.dataset.src;
      lazyImage.addEventListener('load', (event) => checkImageSize(event.target));
      lazyImageObserver.unobserve(lazyImage);
    }
  });
});

const selectors = "#content article .body img, #content article .similar img";
const lazyImages = [].slice.call(document.querySelectorAll(selectors));

if ("IntersectionObserver" in window) {
  for (let image of lazyImages) {
    const src = image.src;
    image.dataset.src = src;
    if (lazyImages.indexOf(image) === 0) {
      const promise = new Promise(resolve => resolve(image));
      promise.then(checkImageSize).catch(setTimeout(checkImageSize, 100))
      continue;
    }
    image.src = "";
  };

  lazyImages.forEach(lazyImage => {
    lazyImageObserver.observe(lazyImage);
  });
}

画像の読み込みが起こったタイミングにフックして画像のサイズチェックを行い、条件にマッチしたら img タグに class を追加し、 CSS のメディアクエリと合わせ技で大きく表示するようにしている。以前やった画像の遅延読み込みのコードを改良した。

その他、 HTML 5 対応が中途半端だったのでマークアップを見直して、適度に <header><secition><article><footer> を使ってマークアップし直した。 このせいだと思うけど Google Adsense の自動広告が差し込まれる位置が変わった。これまでヘッダーの下に入り込んでいたやつが <header><article> の間に入るようになった。

今後も外出禁止が続いたらひたすら自分のブログをいじってしまう予感。こういうエネルギーを仕事とか個人サービス開発とかに当てられるとよいのだろうけどなかなかそういう方向には向かない。

| @労働

COVID-19.jpg

Basecamp の Jason Fried が Amazon で REMOTE がバカ売れし始めたので返金を始めたそう。なぜ返金するのかというと、本が売れているのは多くの人がリモートワークを恐れており、そのような恐怖・不安に REMOTE という本が役立つから、とのこと。

アメリカはリモートワーク先進国なのかなと思っていたけど、やっぱり昔ながらの方法で仕事してた会社もあったんだろう。アメリカでもコロナウイルスが猛威を振るいつつあるいま、そのような会社もリモートワークをやるようになり、リモートワークの心構えが書いてある本がバカ売れしてるということですね。おもしろい。

HashiCorp の Mitchell Hashimoto さんのリモートワークについてのツイートもおもしろかった。これまでのリモートワーク経験で得られたことが書かれている。

全従業員をリモートワークに、という動きは良い傾向だと思うが、たくさんの不安・疑念・不信を引き起こす。多くの人が「はいはいこんなもんね」という感じで仕事をするだろうが、リモートワークは一筋縄ではいかない。多くの人が仕事に使う机を会社のものから自宅の自分のものに置き換えただけのことではないな、ということに気がつくだろう。

リモートワークは一部の人にとってはうまく機能しない。みんながリモートワークに適応できるわけではなく、それは仕方がないことだ。これまで HashiCorp ではたくさんの人が「仕事は楽しい、同僚も好きだ、ただやっぱり自分は顔をつきあわせたコミュニケーションの方がいい」という理由で会社を去って行った。これは自然なことだ。

このほかにも Hashimoto さんが新しく従業員を雇ったときに説明するリモートワークについての心構えが書かれていてうなずきながら読んだ。一番最初のやつから結構ショッキングだが事実だと思う。

「君は友だちができない」。残酷だが事実だ。リモートワークをするなら、仕事以外でとても仲の良い友だちがいないとダメだ。なぜなら仕事では友だちができないから。リモートワークでも同僚とは仲良くなれる。でもリモートワークの同僚とは遊びに行ったり、子ども同士が同じ学校になる、ということはない。

そのほかのツイートもおもしろいので読んでみて下さい。

2020-03-14 追記

Jason Fried が元の Tweet を消してしまっている。およそ 400 件の返金請求が来たみたい。自動化できておらず破滅状態なので返金は終了するとのこと。

| @ブログ

Rubbish

ブログで使ってる Amazon S3 のバケットの画像のほとんどをミスって空ファイルにしてしまった…。 S3 に上がっている画像、 Cache-Control ヘッダーが付与されていないのですべての画像ファイルに Cache-Control ヘッダーを付与しようとしての事故だった。ファイル一覧を取得して AWS SDK Ruby で Cache-Control だけ付与するつもりだったのにファイルそのものを空で上書きしてしまって無となった。

大した操作じゃないと思って事前にバックアップを取っていなかった& S3 に上げておけば安心だと思って日常のバックアップも行っていなかった。スーパーアホ。写真ならアップロードし直すことは可能だけど、キャプチャとか、アニメーション Gif とか、ブログの内容に合わせて OmniGraffle で描いた画像は基本的には元のファイルが残ってなくて元に戻すことができなかった…。

CDN に残っていたものとローカルでキャッシュされていたものを探したが、 500 ファイル以上が失われてしまった。つらい…。

画像の吹っ飛び、過去にも何回か起こっている。レンタルサーバーの障害で消えたパターンもあったけど、自分のミスで消してしまうパターンが多い。自分が一番信用ならないので画像はプロに管理してもらうのが一番だなと身にしみて思った…。とりあえず S3 バケットのバージョンコントロールを有効にしたいと思います…

追記

AWS SDK Ruby での正しい Cache-Control ヘッダー付与の仕方は以下だった。

object.copy_from(object, cache_control: 'max-age=2592000,s-maxage=31536000', metadata_directive: "REPLACE")

#copy_from の引数に medata_directive: "REPLACE" を渡す必要がある。

🙅🏻‍♀️🙅🏻‍♀️🙅🏻‍♀️以下は NG なので注意されたし ☠️☠️☠️

object.put(cache_control: 'max-age=2592000,s-maxage=31536000')

これやるとファイルの body が空になります 🈳🈳🈳

| @旅行/散歩

クヒオビーチ

マイルを貯めるのは低所得の自分には無理だという記事を書いた。

しかしこの記事を書いたあともセブンイレブンで 100 円のコーヒーを買うときもクレジットカードで払ったり、トイレットペーパーを LOHACO で買うときもポイントサイトを経由したり、通りすがりの自販機の釣り銭受けをまさぐって取り忘れの小銭をあさる中学生のようにちまちまマイルを貯め続け、なんとか家族三人そろって特典航空券(エコノミー)でホノルルまで行ける程度のマイルを貯めることができた。金持ちだと毎年ハワイにビジネスクラスで行けるくらいマイルが貯まるみたいだけど、自分の場合は 3 年かかってエコノミー(ローシーズン、 3 人分で 105,000 マイル)が限界だった。

なぜハワイなのか

2013 年に買った POPEYE がハワイ特集号で、「ハワイには若いうちに行こう」と書いてあった。自分はそのとき 32 歳で、コテコテのリゾート地に興味なかったのだけど POPEYE の特集を読んでみると「確かに良いな」と思えたので、なるべく年を取る前にハワイに行っておきたいなと思っていた。

ここ一年半くらい、身を粉にしながら働くものの大した成果を残すことができず、完全に人間として腐り始めていた。このままでは玄界灘で魚の餌になるくらいしか社会貢献の方法がないと思ったので、休みを取って旅行に行ってみることにした。

旅の準備

福岡から成田へ移動

特典航空券界の常識をわかってなくて、マイルが貯まったらすぐに特典航空券を申し込めるのかと思っていたけどそういうわけではなさそうだった。マイルが貯まったので「来月旅行にでも行くか」とほくほくしながら特典航空券を申し込もうと ANA のサイトを覗いたら全然席が空いてない。ハワイのような人気路線の特典航空券は申し込めるようになった瞬間全部埋まってしまうみたいだった。 ANA の国際線の特典航空券の予約は 355 日前から可能になり、マイレージクラブのランクが高い人(= @t32k さんなどの金持ち)ほど申込みが優遇されるシステムなので、ランクの低いマイレージクラブ会員(=貧民)が特典航空券を申し込もうとしたらほぼ一年後の日付にするしかない。手持ちの一番古いマイルの有効期限が 2019 年の 2 月だったので、この時期に本当に旅行に行けるかどうかもわからないまま 2019 年の 2 月末に 2020 年 2 月のチケットを取った。申し込んだとき、どこに行くかとか何をするかとかどこに泊まるかとか何も考えられていなかった。やっと旅の予定を決め始めたのは 2020 年の正月だった。その後コロナウイルスの話が出てきて一旦は旅行を取りやめようかとも思ったが、先延ばしにすると逆に騒動が広がりそうだと思って予定通り 2 月中旬に出発した。

成田で暇つぶし

ハワイに行ってどうだったか

よかった。

カジュアル

ヨーロッパを旅行するときは結構緊張したが(人種差別とかマナーに気をつけたりで気をつかう)、ハワイは気楽でとてもよかった。

アメリカ人に対する良くないイメージが払拭できたのもよかった。少なくともハワイに住んでたりハワイに旅行に来てるアメリカ人からは、ヨーロッパの安宿で見かけるような声がでかくて何でもアメリカ流を押し通そうとする感じの悪さがなくて、嫌な気持ちになることなく旅行できた。

快適な移動

レンタカーや Uber 、 Lyft で移動したので移動で荷物を抱えて大変な目に遭ったり、タクシーにぼったくられないか心配したりしなくて済んだのが良かった。海外で車を運転するのは初めてだったが、ハワイの道は広くてとても運転しやすかった。

スーパークレジットカード社会

何でもカードで払えるので両替する必要がない。現金はチップを払うときか空港の自販機で飲み物を買うときくらいしか使う機会がない。 ABC ストア(ワイキキに 50m 間隔である旅行者向けのコンビニ)で現金で金払ってるのは日本人旅行者くらいだった。

eSIM 便利

今回は自分の iPhone 11 も嫁さんの iPhone XR も eSIM に対応していたので、香港の 3 というキャリアの eSIM を利用してハワイでも日本にいるとき同様に携帯が使えたのが良かった。 5 年前にギリシャに行ったときはちまちま Pocket WiFi をオンにしてインターネットにつないでいたので不自由極まりなかった。

ホテルの取り方

宿は Hotels.com と HIS で取った。 Hotels.com が最安かというとそういうわけではなく、マリオットなど結構良いホテルのチェーンだと HIS の方が料金安い&朝食付きだったりしてお得だった。

旅程

旅程 宿泊
Day 1 家 🚕 福岡 🛩 成田 ✈️ ホノルル 🛩 ヒロ Hilo Hawaiian Hotel
Day 2 ヒロ 🚙 アカカの滝 🚙 MKVIS Hilo Hawaiian Hotel
Day 3 ヒロ 🚙 ワイピオ渓谷 🚙 コナ King Kamehameha's Kona Beach Hotel
Day 4 コナ 🛩 ホノルル Queen Kapiolani Hotel
Day 5 ホノルル 🌴 Queen Kapiolani Hotel
Day 6 ホノルル 🚙 ノースショア Queen Kapiolani Hotel
Day 7 ホノルル ✈️ 成田 🛩 福岡

ハワイ島とオアフ島に三泊ずつした。福岡空港から成田へ行き、成田で 5 時間時間を潰してホノルルへ。ホノルルからハワイアン航空に乗り換えてハワイ島のヒロへ。ヒロに二泊してコナへ移動し、コナで一泊してからコナ空港からまたハワイアン航空でホノルルへ。なおコナではホテルにパスポートを忘れて大騒ぎとなったが、ハワイアン航空の職員の人がめっちゃ親切でスーパー助かった。ギリシャでエーゲ航空に散々な目に合わせられたのとは大違いだった。ホノルルに三泊して成田経由で福岡に戻った。 6 泊 8 日の旅(一泊は飛行機の中)。

ハワイ島

ホテルの部屋から見るマウナケア

ハワイ島ではアカカの滝、マウナケア(中腹まで)、ワイピオ渓谷、コナブリュワリーの工場レストランなどを訪れた。主にレンタカーでドライブしていて、ハワイ島の北半分をぐるっと回った。

アカカの滝

マウナケア

ワイピオ渓谷

ハワイ島はハワイ諸島の中では一番新しい島でまだ火山の荒々しい感じが残ってる。マウナケアは 4200m もある高い山だが、裾野が広がっていて草原が広がり、景色が生まれ故郷の阿蘇に似ていて海外旅行に来てるのに帰省してるみたいな感じで不思議な感覚だった。

ハワイ島の形式

オアフ島

オアフ島ではワイキキに滞在しつつ、ビーチで遊んだり、街をうろついたりした。

ワイキキビーチ

オアフ島でも一日は車で遠出して、ノースショアまで行ってパタゴニアで環境に配慮しながら家族で爆買いしたりした。

ノースショアハレイワ

patagonia で爆買い

ダイアモンドヘッドが見える宿に泊まっていて、できれば登りに行ってみたいと思っていたけどうだうだ無為に時間を過ごしてしまってついぞ登ることができなかった。残念。

ホテルの部屋から見るダイアモンドヘッド

ワイキキは福岡でいうと西通りといった趣でひたすらショッピングするのが好きな人にはよさそうだが少々退屈だった。ライドシェアの Lyft で、カカアコという昔は治安が悪かったけど最近は小洒落た店が増えているエリアに行ってオシャンティなレストランでラム肉を食べたりした。

ラムチョップ

POPEYE で紹介されていた Ray's Cafe という店が安くて巨大なリブアイやロブスターが食べられるようだったが、治安の悪いエリアにある地元民向けのレストランのようで閉店時間が早く、今回は行くことができなかった。

旅行中のツイート


正直、ハワイは一回行けば充分だろうと思っていたが、旅行から帰ってきてからは「またハワイに行きたい」という気持ちが日々募っていく一方だ。酒のやまやに行ってコナビールやフラ印のポテトチップスを買ったり、中公新書のハワイの歴史についての本を読んだり、Amazon Prime ビデオで HAWAII FIVE-0 というドラマを見たりしている。

HAWAII FIVE-0 ではよく主人公が山登りに行っている。ハワイといえば海というイメージあるけど、ダイヤモンドヘッド以外でも登り甲斐のありそうな山がたくさんある。今度は山に登ったり、地元民向けの治安が悪いエリアの店にも乱入してみたりしたい。

ワイキキの夕景

それにつけても金の欲しさよ。

| @技術/プログラミング

キャッシュ

CloudFront 転送量削減の試みで色々やっていて、そういやウェブサイトのキャッシュについての説明記事っぽいの読んだことないし、開発現場だと「じゃあキャッシュすればいいじゃん」みたいな発言が行き交うけど、ひとくちにキャッシュと言っても色々あって、ウェブ開発始めたばかりの人にはよく分からないんじゃないかと思ったので書いておきます。

キャッシュの目的

キャッシュの目的.png

キャッシュの目的は大きく二つあって、コンテンツ表示の高速化とコストの削減がある。コンテンツの表示高速化は、時間のかかる処理の結果を捨てずに再利用して二回目以降の表示を高速化すること、コストの削減は、処理結果の再利用によってコンピューターの利用時間を減らしたり、データの通信量を減らすことをそれぞれ目的としている。表示高速化とコスト削減はどちらか一方だけを達成するものではなく、多くの場合で両方が同時に実現される。

キャッシュに向いているコンテンツ

キャッシュには向いているコンテンツと向いていないコンテンツがある。

キャッシュに向いているコンテンツ.png

ウェブサイトで配信するものに関して何でもキャッシュできるわけではなく、多くの人が共通して閲覧するものか、更新頻度が低いものでないとキャッシュしてはいけない。更新頻度が低くて多くの人が共通して閲覧するもの(画像、 CSS 、 JavaScript など)はキャッシュしやすい。だがショッピングサイトの購入履歴などは人それぞれなので一律にキャッシュしてはいけない。

キャッシュの種類

キャッシュにはブラウザーキャッシュとサーバーサイドのキャッシュがある。

キャッシュの種類.png

ブラウザーキャッシュ

ブラウザーキャッシュは、同じ人が繰り返し訪問するサイトで繰り返し利用されるサイト内画像や JavaScript 、 CSS (静的コンテンツ)に対して提供するのに向いている。二回目以降のアクセス時に前回端末にダウンロードした内容を使い回し、転送量を削減してユーザーの閲覧体験を高速化する。

ブラウザーキャッシュ図解.png

同じ人が繰り返し訪問しないタイプのサイト( SNS でバズって多くの人が訪れるが、一ページ見ただけで離脱するようなサイト)ではブラウザーキャッシュをフルに効かせても転送量の削減効果はない。逆に一度訪れた人が何度も訪れるような再訪率の高いウェブサイトでは、静的コンテンツに対して Cache-Control ヘッダーを付与することでブラウザーキャッシュを効かせ、転送量の削減とコンテンツの表示速度を高速化させることができる。

ブラウザーキャッシュを聞かせるには、適切な HTTP ヘッダー( Cache-Control ヘッダーなど)を付けてレスポンスを返せばよい。

サーバーサイドキャッシュ

サーバーサイドでキャッシュできるのは多くの人が共通して閲覧する、更新頻度の低いデータだけということに注意が必要。ログインが必要なサイトで、アクセスする人によってコンテンツを出しわけないといけないようなシステムではサーバーサイドのキャッシュはあまり利用できない。

サーバーサイドキャッシュ図解.png

サーバーサイドのキャッシュには転送量の削減効果はない(少なくとも自サイト訪問者との通信では)が、ランキング集計処理や外部 API の呼び出しなど CPU への負荷や時間がかかる処理の結果をキャッシュし、コンテンツの表示を高速化したいときに有効。

例えばこのサイトでは Amazon のアフィリエイトを利用しているが、 Amazon の API にリクエスト回数制限やリクエスト間隔制限があるので適度にキャッシュを行い、リクエスト回数制限に引っかからないようにしている。

サーバーサイドのキャッシュのやり方は色々ある。アプリケーションにキャッシュするためのコードを書いてメモリ上に保持したり、テキストファイルに保存したり、データベースに保存したり、 Redis などを使ったり。ウェブサーバーのキャッシュを使う方法もある。このブログでもいくつかのキャッシュ機構を組み合わせている。

アプリケーションキャッシュ.png

表示に関わる部分の一部をキャッシュする場合や、キャッシュの無効化をアプリケーションでコントロールしたい場合はアプリケーションでキャッシュする必要がある。

ウェブサーバーキャッシュ

レスポンス全体をキャッシュしてよい場合にはウェブサーバーのキャッシュを使うとよい。その方がアプリケーション層までリクエストが届かず、コンピューターリソースを節約できる。

様々なキャッシュの組み合わせ

更新頻度が低く、多くの人が共通して閲覧するコンテンツはサーバーサイドでもキャッシュできるしブラウザーキャッシュを効かせることができる。画像、 CSS 、 JavaScript などがそれで、これらのファイルはよく CDN ( Content Delivery Network )から配信される。 AWS だと CloudFront というのがある。この手のサービスはサーバーサイドのキャッシュとブラウザーキャッシュを効かせることに特化したもので、コンテンツの転送量を抑えつつウェブサイトの表示速度を高速化してくれる。

CDN もそうだが、サーバーサイドキャッシュとブラウザーキャッシュを併用すると、コンテンツを更新したいときに問題が出てくる。画像ファイルを新しいものに差し替えたが CSS が更新されず古いキャッシュが参照され、画像が非表示になってしまったり、ということが発生する。そういうことが起こらないように、 HTTP にはいくつか仕組みがある。

CDN だと ETag も自動付与してくれて、コンテンツの衝突を抑えてくれる。 CDN はよくできているのでよく分からない人はまずは CloudFront を使って勉強してみるとよいだろう。

特に注意が必要なのがアプリケーションキャッシュとウェブサーバーキャッシュの組み合わせだ。異なるレイヤーでキャッシュを効かせてしまうと、キャッシュを無効化したいときに狙い通りに無効化されず、意図しない障害になってしまったりする。コンテンツの特性に応じて、キャッシュは一つのレイヤーで行うようにするとよいだろう。画像や CSS はウェブサーバーでキャッシュし、更新頻度が低いが動的に生成される JSON はアプリケーションレイヤーでキャッシュするなど。

まとめ

  • ウェブサイトのコンテンツは何でもキャッシュすれば良いわけでない
  • サーバーサイドのキャッシュとブラウザーキャッシュの違いを理解する
  • 多段キャッシュに注意(特にアプリケーションキャッシュとウェブサーバーキャッシュの併用)