every Tech Blog

株式会社エブリーのTech Blogです。

Apache SparkのSparkSQLのstack関数を用いてデータを横持ちから縦持ちにする

f:id:nanakookada:20210729160653p:plain

はじめに

はじめまして。 データストラテジストの田中です。普段は『DELISH KITCHEN』レシピ視聴実態の可視化やオーディエンス配信のレポート作成、サービス好意度の分析などの業務を行っています。

サービス好意度など定性的な要素が多い分析ではWEBアンケート調査のデータを活用していますが、WEBアンケート調査のローデータは質問内容がカラムとして横持ちで存在することが多いのが特徴です。
今回はデータベースでも扱いやすいよう「Apache Spark環境下で横持ちのデータを縦持ちにする」TIPSをお伝えします。

stack関数を用いて縦持ちにする

Spark SQLにてデータを横持ちから縦持ちにするにはstack関数を使用します。
stack関数については、Apacheより公式ドキュメントが提供されていますので、詳細は下記リンクをご覧ください。

Apache公式ドキュメント

サンプルデータ

横持ちのサンプルデータとして、アンケートデータに近い形のものを用意しました。

+------+------+---+------+--+--+-----------+--+
|sample|gender|age|  area|Q1|Q2|         Q3|Q4|
+------+------+---+------+--+--+-----------+--+
|   AAA|     1| 40| Tokyo| 1| 5|    特になし| 0|
|   BBB|     2| 15| Shiga| 2| 6| アプリを見て| 1|
|   CCC|     1| 20| Osaka| 3| 7|   広告を見て| 0|
|   DDD|     1| 55|Nagoya| 4| 8|       null| 1|
+------+------+---+------+--+--+-----------+--+

各ユーザID(sample)毎に付帯情報(gender 〜 area)と質問内容(Q1 〜 Q4)の回答結果が1行ずつ積まれています。今回はユーザIDと付帯情報に紐づく形で、質問内容を縦持ちにしたい場合の実装例を提示いたします。

実装方法

ドキュメント通りですとstack関数はSELECT stack(n, col1, col2 ...)と記述し、「col1, col2 ...」を「n」行で分割するといった仕様になります。
サンプルデータではSELECT stack(4, Q1, Q2, Q3, Q4)と記述しても良いのですが、実際のケースシナリオを想定した場合、汎用的に使えるよう質問内容のカラム情報を動的に取得できることが望ましいです。汎用性を加味した実装例を以下に提示します。

実装例

import org.apache.spark.sql.functions._

val result = data.columns.filter(x => x.contains("Q")).map{
  v =>
    data.select($"sample", $"gender", $"age", $"area",expr(s"stack(1,'$v',$v) as (q_id, q_value)"))
}.reduce(_ unionByName _)

display(result)

質問内容がスケールすることを考慮し、columnsを使用しカラム名を取得、filterを使用し質問内容のカラム名のみ抽出を行っています。 抽出したカラム名の値をmapにて、1つずつ読み出し、縦積みにしています。$vのようにカラム名を指定すると回答結果だけが縦積されるため、カラム名も'$v'で取得する形にしています。
最後に質問内容のカラム情報を1つずつ縦積みしたものをreduce(_ unionByName _)で結合しています。

出力結果

各ユーザIDと付帯情報に紐づく、質問内容のカラム名q_idと回答結果q_valueの一覧を出力することができました。

+------+------+---+------+----+------------+
|sample|gender|age|  area|q_id|     q_value|
+------+------+---+------+----+------------+
|   AAA|     1| 40| Tokyo|  Q1|           1|
|   BBB|     2| 15| Shiga|  Q1|           2|
|   CCC|     1| 20| Osaka|  Q1|           3|
|   DDD|     1| 55|Nagoya|  Q1|           4|
|   AAA|     1| 40| Tokyo|  Q2|           5|
|   BBB|     2| 15| Shiga|  Q2|           6|
|   CCC|     1| 20| Osaka|  Q2|           7|
|   DDD|     1| 55|Nagoya|  Q2|           8|
|   AAA|     1| 40| Tokyo|  Q3|      特になし|
|   BBB|     2| 15| Shiga|  Q3|   アプリを見て|
|   CCC|     1| 20| Osaka|  Q3|    広告を見て|
|   DDD|     1| 55|Nagoya|  Q3|        null|
|   AAA|     1| 40| Tokyo|  Q4|           0|
|   BBB|     2| 15| Shiga|  Q4|           1|
|   CCC|     1| 20| Osaka|  Q4|           0|
|   DDD|     1| 55|Nagoya|  Q4|           1|
+------+------+---+------+----+------------+

また別のサンプルデータを元に、Databricksのdisplay関数でプロットを作成しました。
縦持ちのテーブルのメリットは下記プロット結果のように質問全体での足し上げがしやすいことと、その他2軸以上のグラフを作成するときにも便利なことです。

サンプルデータ

f:id:blacktiger126:20210729144746p:plain
サンプルデータ

プロット結果

f:id:blacktiger126:20210729140324p:plain
サンプルプロット

最後に

実装自体はシンプルですが、stack関数の実装例が少ないと感じたため取り上げてみました。
最後まで閲覧いただきありがとうございました。

DELISH KITCHENチラシの郵便番号・地域名・店舗名検索実装について

f:id:nanakookada:20210721185210p:plain はじめまして。DELISH KITCHEN開発部でバックエンド開発等に携わっている南です。

今回は2021年4月の中旬にリリースされた、「DELISH KITCHENチラシの郵便番号・地域名・店舗名検索実装」の裏側をお話したいと思います。

f:id:takahiro_minami:20210720132515p:plain
DELISH KITCHEN チラシ

検索エンジンによる、郵便番号・地域名・店舗名検索

DELISH KITCHENチラシにはもともと郵便番号検索機能がありましたが、今回、その郵便番号検索の入力欄に郵便番号・地域名・店舗名、いずれの文字をいれても検索できるよう機能拡張しました。 1つの入力欄で郵便番号・地域名・店舗名検索をできるようにするにあたり、今回はElasticsearchを用いました。

f:id:takahiro_minami:20210720132442p:plain
Elasticsearch

n-gram vs. 形態素解析

検索エンジンで用いる際は、文字列をどのようにトークン化するかが重要になってきます。 検索エンジンのトークン化といえば半角スペース区切り、形態素解析、n-gramあたりが主流です。

昔の話ですが、カーナビの目的地住所検索ではn-gramを使用していると聞いたことがありました。 そこで最初にn-gramによるトークン化を試してみましたが、本来1位付近に表示したかった店舗が、他の店舗に埋もれてしまうという悪い結果に終わりました。

地域名・店舗名で検索される方は、短いワードで検索することが予想されます。 その短いワードに「海、川、木、山」や「東、西、南、北」など地域名に頻出するワードが含まれていると、大量の結果が返ってきてしまいます。 (昔のカーナビの住所検索は、住所を8-9割入力してようやく絞り込みができたな・・・、ということを思い出しました。)

n-gramでは良い検索体験を得られないことが分かったので、DELISH KITCHENチラシの郵便番号・地域名・店舗名検索では形態素解析することにしました。

郵便番号・地域名・店舗名検索と形態素解析

まず郵便番号は数字&記号であるため、形態素解析ではなくもっとシンプルな解析器を用いました。これについては後述いたします。

地域名・店舗名は、いずれも日本語の非分かち書きではありますが、文章ではありません。 形態素解析をするというより、辞書を充実させて形態素解析器に名詞判定してもらいトークン化する作戦です。 辞書の優劣が結果の優劣に直結してきます。

郵便番号の解析器

郵便番号検索は日本語を含まないため形態素解析も辞書も不要です。 ただし郵便番号検索では、例えば 「106-6238」とハイフン付きの7桁で検索するユーザーと「106」と3桁で検索するユーザーへの対応が求められます。

そこで「106-6238」のハイフンを「106 6238」(半角スペース)に置換したのち、半角スペース区切りでトークン化する解析器を用意しました。 indexに [106, 6238] とリストとして情報をもたせておくことで、106-6238(106 AND 6238) で検索されても106のみで検索されても〒106-6238の店舗を検索結果に含めることができます。

{
    "settings": {
        "analysis": {
            "char_filter": {
                "hyphen_to_space" : {
                    "type" : "mapping",
                    "mappings" : ["-=>%"]
                }
            },
            "analyzer": {
                "postal_code_analyzer": {
                    "type": "custom",
                    "tokenizer": "standard",
                    "char_filter": [
                        "hyphen_to_space"
                    ],
                    "filter": [
                        "split_delimiter"
                    ]
                }
            }
        }
    }
}

地名辞書データの作成

エブリーにはデータ分析業務を行っているData&AIチームがあり、地名の読み仮名データを過去に作成していたため、それを活用して地名辞書を作成しました。 ただし漢字一文字の地名は、細かくトークン化されてしまいn-gramのようになってしまう恐れがあるため削除しました。 また長すぎる住所は、辞書の1単語としてふさわしくないためこちらも削除しました。

検索対象となる店舗の住所の大半は市街地になります。「漢字2-5,6文字の地名さえ網羅できればよいだろう」くらいの気持ちで辞書を作成しました。

...

左曽,左曽,サソ,地域
巨勢,巨勢,コセ,地域
布佐,布佐,フサ,地域
布勢,布勢,フセ,地域
布太,布太,フダ,地域
布施,布施,フセ,地域
布木,布木,フキ,地域
布瀬,布瀬,フゼ,地域
布良,布良,メラ,地域

...

店舗名辞書データの作成

こればかりは、人力で作成するほかなかったため、DELISH KITCHENのデータベースから店舗名一覧を取り出して1つ1つ読み仮名を振っていきました。 幸いにも膨大な数ではなかったので手作業でこなすことができましたが、単純作業というわけにはいきませんでした。

たとえば店舗名が「エブリー商店」だった場合、「エブリー商店」で検索するユーザーもいれば、「エブリー」のみで検索するユーザーもいるでしょう。 そこで「エブリー」で1単語、「商店」で1単語、辞書作成することにしました。そうすることで「エブリー」でも、「エブリー商店」でも検索できるようになります。

店舗名は凄くユニークな店舗名もあれば、一般名詞や人名の店舗名もあります。1つずつ店舗名を確認し「どうのように検索されるだろうか?」「自分ならどんな検索をするだろうか?」と考えながら辞書作成を行いました。

kuromoji_iteration_mark filterに注意

リリース直前に「代々木」で検索できないという報告があがりました。 これはkuromoji_iteration_markをfilterに設定していたことが原因でした。iteration_markとは踊り字、つまり代々木の「々」を意味します。 kuromoji_iteration_markを設定すると、検索エンジンが踊り字を前の漢字に変換してしまいます。「代々木」で検索すると、「代代木」に変換されます。「代代木」という単語は地域名辞書には存在しないため、「代/代/木」とトークン化されていたのが不具合の原因でした。

地域名辞書に「々」を含む地域がいくつあるか確かめてみたところ170個ほど存在しました。 さほど多くはないのですが、幸運にも「代々木」という有名な地域名があったため気がついてくれた方がいました。安易にkuromoji_iteration_markを使うと辞書とマッチしなくなるため注意しないといけません。どうしてもkuromoji_iteration_markを使わなければならない場合は、辞書に「代々木」と「代代木」の両方を含めないといけません。

...

久々知,久々知,ククチ,地域
久百々,久百々,クモモ,地域
久野々,久野々,クノノ,地域
代々木,代々木,ヨヨギ,地域
佐々木,佐々木,ササキ,地域
佐々生,佐々生,サソウ,地域
佐々礼,佐々礼,サザレ,地域

...

最後に

今回は、郵便番号・地域名・店舗名検索公開に至るまでに悩んだことや躓いたことの地道な活動をまとめてみました。

検索結果の良し悪しにゴールはありません。今後、提携する店舗が増えていけば、それに伴った調整も必要になりますし、ユーザーの声にあわせた調整も必要なります。良い検索結果を返し続けるためにも、絶え間なく改善活動を続けていきたいと思っています。

社内でkubernetesの輪読会を開催しました

f:id:rymiyamoto:20210714113901p:plain

社内でkubernetesの輪読会を開催しました

はじめに

こんにちはMAMADAYS バックエンド担当エンジニアの宮本です。 今回は私の所属している開発チームでkubernetes(以下k8s)の輪読会を行ったので、その内容を紹介していきます。

MAMADAYSのサービスやバックエンドシステムの全体像については MAMADAYSのサービスとバックエンドシステムのお話 にて紹介していますので、よろしければご覧ください。

経緯

現状MAMADAYSのバックエンドシステムはAWSのEKS上で運用されています。 しかしk8s周りを触っているのが特定のメンバーのみとなっており、チーム内で知識にばらつきがありました。 またそのメンバーも体系的な学習を行っているわけではなく、十分な理解がない状況でもありました。 そのためメンバー内のSREから「チーム全体でk8sへの理解を深める必要があるのではないか」という意見が出され、チーム全員で輪読会を行う流れになりました。 私のいるバックエンドチームはweb開発も行っており、web担当メンバーも自主的に参加し実施されました。

輪読会とは

参加しているメンバーが同じ書籍を事前に読んできて、その内容について意見を交わす会です。 事前に決められた担当者が本の内容を要約し、他のメンバーが理解できるような形で発表を行います。 複数人で同じ書籍をそれぞれの視点から読み解くため、個人では理解が難しい部分をフォローしあうことで、よりメンバー間での知見が深まるようになります。

社外も含めた輪読会は実績もなくハードルが高いため、今回はチームメンバーの知識のベースアップと実際に業務で用いている部分を見比べながら行うことに重きを置くようにしました。 またコロナの感染状況も考慮して、Zoomを使ったオンライン開催となりました。

利用した書籍

インプレス社から出版されている Kubernetes完全ガイド 第2版 を用いて行われました。

f:id:rymiyamoto:20210714111531j:plain
Kubernetes完全ガイド 第2版

本書はk8sに関する機能でアプリケーションエンジニアが利用する可能性が高いものを網羅的に解説されており、様々なユースケースが紹介されています。 体系的に説明されていて図による視覚的な理解を得やすく、サンプルが添付されているためこちらを利用することとなりました。

運用

輪読会の進め方は様々ありますが、今回は知識のベースアップが目的のためチームメンバーが週一回入れ替わりで担当。事前に対象となるページを決めて、その内容をスライドにまとめて議論していく方針を取りました。 このときの進行役としては発案者であるSREのメンバーが執り行ってくれました。

全体の流れとしては以下のとおりです。

  1. 開催1週間前までに対象となるページと発表者を決める。
  2. 各メンバーが対象箇所を読んでおく、また発表者はさらにその箇所をスライドにまとめる。
  3. 当日にスライドを使って発表(30分程度)し、残り時間で質疑応答

業務を圧迫しないように、毎週金曜日開催で水曜日の時点で間に合わない場合はスキップも可としています。 緩くですが確実に進めれるようにしました。

実際やってみて

輪読会は週1ペースで全19回にわたり緩く行われ、読破までには6ヶ月弱かかりました。 本書に書かれていることはもちろんためになりましたが、さらに輪読会をする上で得た経験としては以下のとおりです。

良かったこと

メンバーによってはベースの知識の差で理解度のブレがありましたが、メンバー内でしっかりと深堀りをしていくことで埋め合わせができました。 チーム全体の知識のベースアップができたのはとても大きかったです。

また書籍の内容については新しい発見もあり、プロダクトで生かせる機能が数多くありました。 例を上げると、Pod起動時のヘルスチェックを Startup Probe で行うことができ早速導入しました。

大変だったこと

書籍のボリュームが多く、読破までの時間がかかってしましました。 ですが、下手に章を飛ばしたりすることなく全体的に学ぶことができました。

また業務や参加メンバーのスケジュールによっては調整が必要でした。 なのであまりかっちりとした予定は組まずに緩く進めるのは重要だったと思います。

最後に

以上、社内でのk8s勉強会の簡単な報告となります。 なんとなく理解している状況は継ぎ接ぎの対応で済ましてしまうため、しっかり時間をとって学ぶことは大変有意義となりました。 k8を新規に採用したり運用している場合は、体系的に学ぶことで今後のユースケースに対応しやすくなると思います。

社内でのk8sの勉強会を検討している方に参考になれば幸いです。

Google I/O 2021で発表されたアプリ内購入の新機能について

f:id:takash1t0m0be:20210706215223p:plain

はじめに

はじめましてDELISH KITCHEN Androidエンジニアの友部です。 私は現在、プレミアムチームに所属しており、主にAndroidの課金が関係している施策などを担当しています。 今回はGoogle I/O 2021で発表されたアプリ内購入の新機能について書いていきたいと思います。

DELISH KITCHENのプレミアムサービスについて

まず、少しだけDELISH KITCHENの話をさせてください。 DELISH KITCHENではプレミアムサービスとして、有料で利用できる機能やコンテンツを提供しています。主な内容は以下で、1ヶ月のアプリ内購入によるサブスクリプションで販売をしています。プランとしては6ヶ月、1年のものも販売していますが、ひと月あたりの金額が安くなるといったもので、内容は同じものです。

  • 機能

    • 人気ランキング
    • 広告非表示
    • 1週間献立
    • すべての栄養成分表示
    • お気に入り無制限
  • コンテンツ(限定レシピ)

    • ダイエット
    • 作り置き
    • ヘルスケア
    • ベビー

今回発表された新しい販売方法について

今回のGoogle I/Oでは新しく3つのアプリ内購入の販売方法が発表されました。 1つ目がMulti-Quantity Purchases2つ目がMulti-line Subscriptionsそして3つ目がPrepaid Plansです。

Multi-Quantity Purchases

f:id:takash1t0m0be:20210706214328p:plain

消費型の商品を複数選択し、購入できるようになります。

今まではある商品を1個売り、10個セット売り、20個セット売り…としていたものをユーザーが必要な個数だけ選択し購入できるようになります。ユーザーにとっては嬉しい仕組みになりそうです。 動画の中ではPlay Consoleより設定できるとのことですがまだ項目が表示されていないためもう少し待つ必要がありそうです。現在DELISH KITCHENでは、そのような消費型の商品は存在していないため出番はないかもしれません。

Multi-line Subscriptions

f:id:takash1t0m0be:20210706214644p:plain

1つのサブスクリプションの一部として、複数のサブスクリプションを販売することができます。

今まで1ユーザーに対して、1つの商品しかサブスクリプションとして提供しないのが主流でしたが(別の商品はアップグレード、ダウングレード扱い)、機能を個別に切り出してユーザーに選択して購入してもらうといったことが可能になります。 例えばDELISH KITCHENの場合なら、限定レシピを分割して提供し、ダイエット、作り置き、ヘルスケア、ベビーをそれぞれをサブスクリプションの商品として販売します。ユーザーは必要なレシピを選択し、購入するといったことができるようになります。 それに加えて、別のジャンルが欲しくなった場合は追加で購入したり、不要になった場合は削除したりできます。

Prepaid Plans

f:id:takash1t0m0be:20210706214650p:plain

一定期間、ユーザーにコンテンツへのアクセスを提供できるようになります。

ユーザーにプレミアムサービスの一部を切り出して提供することで、プレミアムサービスの価値を感じてもらった上で通常のプレミアムサービスを購入してもらうといったことが可能になります。 具体的には、機能の広告非表示の部分だけを提供したり、限定レシピのうちいくつかだけを提供したりするようなイメージです。 有効期限が切れそうになるとユーザーに通知が届くので、そのタイミングでアップデートしてもらえるようにするのが良さそうです。このPrepaid PlansReal-time developer notificationsSubscription APIなどもサポートされます。

最後に

2021/5/18にこれらの発表があり、Billing Library 4.0は提供されたもののこれらの機能自体はまだ使えないようです。

しかし、Googleが公式に提供しているアプリ内課金のサンプルコード(Google Play Billing Samples) にもこれらの機能を示唆するコメントがコード内に記載されていたので間もなく使えるようになるでしょう。公開されたら実際に使ってみて次の機会にブログに書いていけたらと思います。

最後までお読みいただいてありがとうございました。

参考:Grow your business with new engagement and monetization features | Session

Core Web Vitals 改善のお話

Core Web Vitals改善のお話のタイトル画像

Core Web Vitals 改善のお話

はじめに

こんにちは。MAMADAYS Web 担当の櫻井です。 以前のエブリーエンジニアブログにて Google の Core Web Vitals (以降 CWV) についてご紹介しました。今回は CWV のパフォーマンス改善について、MAMADAYS の Web チームが実際に行ったこと、またその結果についてをご紹介したいと思います。

ゴールとしては CWV の3指標 FID, LCP, CLS を合格基準にすることと、ベンチマークしているサイトよりも優れた数値に改善する*こととしました。

*Google の公式 FAQ によると、CWV が検索ランキングシグナルとして使われるケースは"tie-breaker"の役割が強いようで、ひとまずは競合の中で上位に入り込むことがページパフォーマンスの恩恵を受ける第一歩となります。 - What is the page experience update and how important is it compared to other ranking signals?

まずは計測してみる

パフォーマンス改善においては何よりもまず現状を計測してみることに始まります。今回はひとまず Page Speed Insights(以降 PSI)でサイトの状態を確認してみました。

MAMADAYS のとある記事ページを測定した結果、2021/01 時点では以下のようになりました。

改善前のスコア_17ポイント

なお、結果の見方としては一番上の数字がこの時点で測定した Lab データから算出されるスコアです。直下の「フィールド データ」は現実のユーザの体験をビッグデータとして Google が蓄積したものを反映した数値です。そのため、上のスコアと下のフィールドデータには必ずしも連動しているわけではありません。

CWV は FID, LCP が Good 圏内で、CLS が Bad 圏内。そしてスコアは 17pt。これはベンチマークしているサイトのページと比較してもワースト1位を競う非常に悪いスコアでした。

改善前のベンチマーク別ランキングのグラフ_スコアが17ポイントでワーストを競う

また Web パフォーマンス改善にあたって、FID, LCP, CLS の各数値については改善の結果をリアルタイムで把握したいため、PSI で表示されるフィールドデータではなくラボデータを取得・蓄積すると良いでしょう。前回の記事で紹介したように MAMADAYS ではラボデータを BigQuery に蓄積し metabase でモニタリングするようにしています。ただし、CLS に関しては正しく値を取得できなかったため、特定の記事の Lab データを毎日取得するスクリプトを作成し、モニタリングを行いました。(API は PSI API を使用)

弱点を特定する

CWV の値をある程度把握できたところで、次にどこの改善に着手すべきかを特定します。

MAMADAYS では CWV 合格へ向けて CLS の改善はもちろん、スコアを底上げするために LCP の改善も目指しました。FID はこの時点で最も合格閾値を超過している(100ms)ため、注力しないことを決めました。

具体的な改善アクションを決める際には PSI や Lighthouse が非常に役に立ちます。これらは当該サイトの何がパフォーマンス的に悪いかを親切に文章で教えてくれる機能を有しています。 PSI の結果の画面をスクロールしてみるといくつかの項目で指摘されていました。

  • Remove unused JavaScript

    使用されていない無駄な JavaScript が読み込まれているようです。tree-shaking を適切に行い、デッドコードの除去をする必要があります。また、そもそも使われていない余分なコードをリファクタして削除するなどの整理をすると良さそうです。なお、今回はここの改善は行っていないため説明は割愛しますが、多くの場合ここを改善すれば大きく LCP が下がることが期待できます。 f:id:sa9sha9:20210630144956p:plain

  • Defer offscreen images

    画像の遅延読み込みがされていないようです。ファーストビューに表示されない画像の遅延読み込みをする必要があります。特にsp_footer_banner@3x.png, babyfood_merit_banner@3x.png はファイルサイズが大きく、かつフッター部分に表示される画像なので必ず遅延読み込みをした方が良い画像です。 f:id:sa9sha9:20210630144828p:plain

  • Remove unused CSS

    使用されていない無駄な CSS が読み込まれているようです。 特に viceo-react.css は使用率が 0%で、かなり無駄なロードになってしまっているようです。 f:id:sa9sha9:20210630144848p:plain

  • Eliminate render-blocking resources

    初回レンダリングを遅くする読み込み方法のリソースがあるようです。MAMADAYS では CSS の読み込みが阻害要因となっており、読み込みタイミングの見直しが必要そうです。 f:id:sa9sha9:20210630144832p:plain

  • Properly size images

    画像のサイズが最適でないようです。ここでも一番上の画像については 93%も太っているようなので、適切にサイズを制限する必要がありそうです。 f:id:sa9sha9:20210630144844p:plain

このように PSI で検査するだけで多くの改善ポイントがあることが把握できました。ただし PSI や Lighthouse ではフロントエンドの改善項目しか検査できません。例えば LCP についてはサーバーサイドやインフラの構成を見直すことでも十分に改善できることを念頭においた方がよいでしょう。

LCP の改善

LCP の改善では大きく改善が進んだポイントが3つありました。 LCPの改善のグラフの全容

無駄なリソースの読み込みを除去

1つ目は2月頭の -2000ms ほどの改善です。

1つ目のLCP改善のグラフ_余分なリソース読み込みを抑止し、2000msほど改善に成功

Remove unused CSS」と「Eliminate render-blocking resources」について対応しました。上記の指摘では、カルーセルを実装する slickや、動画プレイヤーのvideo-reactが挙げられています。

Chrome Developer Tool の Coverage 機能で確認すると確かに無駄な CSS であることがわかります。 不要なリソースが読み込まれている図

MAMADAYS では CSS を Next.js の CSS Modules で読み込みをしているため、これらの読み込み箇所を _app.js から実際に必要なコンポーネントに移動しました。これにより不要なCSSの読み込みが除去されたことがわかります。 不要なリソースが読み込まれなくなった図

http2 への切り替え

2つ目は3月頭の -2200ms ほどの改善です。

2つ目のLCP改善のグラフ_画像のレスポンスをhttp2に対応させたことで2200msほど改善に成功

これは Web 内で使われる画像の配信元 CDN のプロトコルを http/1.1 から http/2 へ切り替えたことによるものです。この切り替え設定自体は2月頭に行っていたものですが、このタイミングで PSI がリクエストを http/2 で行うように変更されたため、数値に大きく改善が現れました。

ref: March 3, 2021 | PageSpeed Insights uses http/2 to make network requests

before 画像のレスポンスがhttp/1.1で返ってきている様子

after 画像のレスポンスがhttp/2で返ってくるようになった様子

画像を適切なサイズで配信

3つ目は3月末の -1800ms ほどの改善です。

3つ目のLCP改善のグラフ_読み込み画像のサイズを最適化したことで1800msほどの改善に成功

PSI での指摘項目にあった「Properly size images」に対応した結果でした。MAMADAYS では画像を CloudFront で配信しており、同時に Lambda Edge にてリサイズを行なっています。取得する画像の URL クエリパラメータに?w=400などサイズを指定することでリサイズできるようにしているため、これを使ってサイズの最適化を行いました。

以下はコードの抜粋ですが、sourcemedia attributeで sp/pc 時の画像サイズを適切に切り分けました。Retina 対応のために 2x 時の指定もsrcset attributeで行うと良いでしょう。

また、リサイズと同時に画像タイプも WebP に変換することでよりサイズの軽量化を行なっています。

const spSize = [imgSize.sp, imgSize.sp * 2];
const pcSize = [imgSize.pc, imgSize.pc * 2];

<source
  type='image/webp'
  media='(max-width: 767px)'
  srcSet={`${src}?w=${spSize[0]}&fm=webp, ${src}?w=${spSize[1]}&fm=webp 2x`}
/>
<source
  type='image/webp'
  media='(min-width: 768px)'
  srcSet={`${src}?w=${pcSize[0]}&fm=webp, ${src}?w=${pcSize[1]}&fm=webp 2x`}
/>
<source type='image/webp' srcSet={`${src}?w=${pcSize[0]}&fm=webp, ${src}?w=${pcSize[1]}&fm=webp 2x`} />
<img
  src={`${src}?w=${pcSize[0]}&fm=jpg`}
  alt={alt}
  loading={loading}
  {...(hasSize ? { height, width } : null)}
  className={classnames(css.image, className)}
/>

改善したものの数値に影響がなかったもの

PSI の指摘項目のうち、数値に影響があまり現れないものもあります。 例えば「Defer offscreen images」は 確かに対応した方が良いものですが多くの場合、遅延読み込みするべき画像はファーストビュー外の画像になるため、LCP の対象になるものが少ないです。そのため効果があまり現れなかったのでしょう。この辺りは改善コストを省みて実施するかを判断するのがよいでしょう。

巨人の肩に乗る(大切)

MAMADAYS Web は React 製で、フレームワークに Next.js を使用しています。Next.js はパフォーマンス観点の積極的な改善を行っており、v10.1 ではバンドルサイズが 58%も縮小されたことで純粋にロードするファイルサイズが小さくなり、LCP の向上につながります。このようにコミッティーの勢いがあり積極的に改善が行われているフレームワーク・ライブラリを選定することもパフォーマンス改善において重要になるでしょう。

CLS の改善

CLS の改善では主に画像の領域確保と広告の領域確保を行いました。 CLSの改善のグラフの全容

3月末の時点で画像のサイズ最適化と同時に width と height を指定して領域の確保を行いました。これにより 20%ほどの改善ができました。

その後に数値が大きく上ぶれてしまっているのですが、これは新機能として CLS と相性の悪い機能を実装したことによるものです。これについてはその後改善を行ったり、CLS の算出アルゴリズムが変わったことで元の水準まで戻していますが、新機能を実装する際には Web パフォーマンスの観点からも慎重に検討を行いたいところです。

CLS の算出アルゴリズムが変わったことで CLS が向上

2021/06 に Lighthouse8 がリリースされました。これを機に CLS の算出アルゴリズムが変更になりました。元々は CLS はページ滞在中全てのレイアウトシフトを累計したものになっていましたが、変更後は 5 秒間の内に発生したレイアウトシフト群の合計の中で最も値の大きいもの、というようになりました。これにより多くのサイトで CLS が改善し、MAMADAYS もその恩恵を受けることができました。詳細については以下をご参照ください。

https://web.dev/cls/#what-is-cls

スコアロジックにも変更あり

また Lighthouse8 ではスコアロジックが変わりました。各指標のスコアに反映される重み付けが変更になったのですが、以下のようになりました。 特に CLS が3倍になっているため、従前よりもレイアウトシフトを起こさない実装を心がける必要があります。

指標 従前 → 変更後
First Contentful Paint 15% → 10%
Speed Index 15% → 10%
Largest Contentful Paint 25% → 25%
Time to Interactive 15% → 10%
Total Blocking Time 25% → 30%
Cumulative Layout Shift 5% → 15%

https://web.dev/performance-scoring/

改善の結果

この時点での PSI を測定してみると以下のようになりました。 フィールドデータは遅れて徐々に反映されるため、変化がないように見え少々分かりにくいですが、特に LCP の改善が効いたことで 20pt ほどスコアが向上しました。 改善後のスコア_37ポイント

これによってベンチマークしているサイトの中でもランキング上位を推移するようになりました。 改善後のベンチマーク別ランキングのグラフ_スコアが32~35ポイントで上位を推移するようになりました

今回はランキング上位に入ることはできましたが、CWV の合格基準に達することはかないませんでした。合格基準を満たせるように、MAMADAYSでは今後も改善を重ねていきます。

まとめ

今回はほとんどがフロントエンドにフォーカスした改善でしたが、より改善を進めるためにはサーバーサイドアプリケーションの改善やキャッシュの改善を行う必要があります。MAMADAYS では今後も継続的によりよい UX を提供するためパフォーマンスの改善を続けていきます。よい UX を作ることやパフォーマンスの改善に興味がある方・造詣が深い方はぜひ RECRUIT | every, Inc. までご連絡ください。