dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

Google I/O'19の「Speed at Scale: Web Performance Tips and Tricks from the Trenches」を観たメモ

途中飛ばしてしまった箇所もありますが、一通り観たのでメモを。

長いので最初にまとめ

動画の最後のスライドにキーワードがまとまってます。

f:id:dackdive:20190604035502p:plain

  • 速くする(Get fast)ための13個のTips
  • 速さを維持する(Stay Fast)ためのTipsとしてPerformance BudgetsおよびLightWallet

が紹介されています。
また、このセッションではパフォーマンス改善の事例が多数紹介されています。

f:id:dackdive:20190604035551p:plain


Performance Budgets

  • パフォーマンスを維持するためのしくみ
    • 予算を管理するように守るべきパフォーマンス指標を設定できる
  • Performance Budgetsでできること
    • Time (TTI ◯秒以下)
    • Resources (JSは◯KB以下)
    • Lighthouse (スコア◯点以上)

f:id:dackdive:20190604035618p:plain

  • Budgetsすなわち予算を設定することで、shipする前にパフォーマンスの問題に気づける
  • 事例: Walmart Grocely
    • PR時にチェックし、バンドルサイズが1%以上増加するとPRは失敗、Issue登録されPerformance Engineerにエスカレーションされる


LightWallet

  • Webサイトのパフォーマンス測定ツールであるLighthouseでPerformance Budgetsをサポート
  • CLI版のLighthouseで使える
    • Lighthouse自体はCLI版とChrome拡張版がある
$ npm i -g lighthouse
$ lighthouse https://example.com --budgetPath budgets.json --chrome-flags="--window-size=1280,660" --view

🤔時間系のBudgetsは指定できない?


画像

Lazy Loading
  • JSのライブラリを使った例だと、こんな書き方をすることが多い
    • LazySizesやReact Lazy Loadなど
<img data-src="images/unicorn.jpg" class="lazyload">
  • 事例: chrome.com サイト
    • 20% Faster page load times (mobile)
    • 26% Faster page laod times (desktop, Windows)
    • 画像サイズに関してはページ初回ロード(initial page)で46%の削減に成功
  • 事例: Netflix

    • たくさんの動画がタイル状に並んだページ
    • 初期描画では最初に見える数行分の画像のみロードし、残りはlazy load
    • ページ全体で4.4MBの画像があるのを、初期描画では1.2MBに抑えることができた
    • メモリだと45MB(Full Page Load) -> 8MB(Initial Page State)
  • ブラウザのAPIで画像のlazy loadをサポート(comming to Chrome this summer.)

    • iframeも
<img loading=eager>
<img loading=lazy>


Responsive Images
<!-- By width -->
<img src="cat.jpg"
  srcset="cat-240.jpg 240w,
          cat-480.jpg 480w,
          cat-960.jpg 960w>


Images CDNs
  • image optimization as a service
    • Cloudinary, Imgix, Thumborなど

f:id:dackdive:20190604035824p:plain

  • 事例: Trivago
    • Image CDNとしてCloudinaryを採用
    • Image CDNを利用することにより、自社のエンジニアはコアビジネスに集中できた
    • 全体の画像サイズを80%削減
  • 日本の事例もいくつか(一休、ANA)

f:id:dackdive:20190604035910p:plain

JavaScript

Defer Third-Party JavaScript
  • third-partyのコードはJS実行時間の57%を占める
  • 事例:Telegraph
    • JSをすべて(third-partyだけでなく自社のものも含め)deferして描画を3s高速化した
  • ref. asyncdeferの違いについては \<script> タグに async / defer を付けた場合のタイミング - Qiita
    • deferはJSの実行順序を保証してくれる
  • 事例: chrome.com
    • トップページに埋め込んでいたYouTube動画をlazy loadしたところTTIで10sの高速化


Remove Expensive Libraries
  • moment.js, jQuery, Bootstrapのようなサイズの大きいライブラリをリプレイスする
    • 例: lodash -> lodash-es, momentjs -> date-fns
    • サイズが小さいことに加え、Tree-shakeableなライブラリを選ぶのも重要
  • 事例: Tokopedia
    • ランディングページをSvelteでリライトした
    • 新バージョンはabove-the-fold contentの描画に必要なJSは37KB
    • Reactで作成した現行バージョンは320KB
    • ランディングページ以外はReactのまま。Service Workerを使ってprecache
  • Update dependencies
    • 事例: Zalando (ヨーロッパのfashion retailer)
      • Reactを15.6.1 -> 16.2.0に上げただけでload timeが100ms向上した
  • Code-splitting


Critical CSS

  • Critical CSS: above-the-fold contentの描画に必要なCSS
  • ドキュメントにインラインで埋め込むべき。14KB以下
  • 事例: TUI (ヨーロッパの旅行サイト)
    • First Contentful Paintの描画を2.4s -> 1.2sに短縮
  • 事例: 日経
    • Critical CSSが300KBもあった
    • シチュエーション(ユーザーがログイン状態か、など)に応じた適切なCritical CSSを返すサーバーを構築
    • ESI: Edge Side Inclusion

Lightning Web Componentsの一部がオープンソース化された

現在開催中の TrailheaDX'19 で発表があったようですね。

記事によると、LWCの機能は以下の3つのレイヤーから成るとし、

  • The Lightning Web Components framework: the framework’s engine.
  • The Base Lightning Components: a set of over 70 UI components all built as custom elements.
  • Salesforce Bindings, a set of specialized services that provide declarative and imperative access to Salesforce data and metadata, data caching, and data synchronization.

今回オープンソースとして公開されたのは、一番下の基本的なレイヤーである framework 部分とのこと。

f:id:dackdive:20190530125155p:plain

(画像は該当のブログ記事より引用)

Salesforceプラットフォーム固有の要素を含まないので、Heroku などの任意のプラットフォームで利用できます。

発表と同時に lwc.dev という充実したドキュメントを用意したり、
create-react-app ライクな lwc-create-app というコマンドで始められたり
ドキュメント内に playground があったりというあたりはさすがですね。

なお、ブログ記事中にはGitHubリポジトリは紹介されてませんでしたが、こちらです。

TypeScript: ReactのContextに型をつける(useContextと16.3以前のLegacy Contextも含む)

TypeScript + Reactでコンポーネントを書くとき、Context を使っているコンポーネントに対してどう型を書くのが正解か迷ったので、調べたことをメモしておきます。

調べるきっかけとなったコンポーネントは React 16.3 以前の Legacy Context を使った書き方になっていたのですが、ついでに新しい Context での書き方と、最新の Hooks の useContext() を使った場合も調べます。

なお、Context および useContext() についての説明は公式ドキュメントに譲ります。

目次


Contextを使う前のサンプルコード

import React from "react";
import "./App.css";

const ThemedButton: React.FC<{ theme: string }> = props => (
  <button className={props.theme}>Click me</button>
);

const Toolbar: React.FC<{ theme: string }> = props => (
  <div>
    Hello, TypeScript & React. <ThemedButton theme={props.theme} />
  </div>
);

const App: React.FC = () => {
  return (
    <div className="app">
      <header className="app-header">
        <Toolbar theme="dark" />
      </header>
    </div>
  );
};

export default App;

ThemedButtontheme を渡すために Toolbar にも props を渡しています。


1. React 16.3 以降の Context を使う場合

import React from "react";
import "./App.css";

const ThemeContext = React.createContext("light"); // (1)

class ThemedButton extends React.Component {
  static contextType = ThemeContext; // (1)
  context!: React.ContextType<typeof ThemeContext>; // (2)

  render() {
    return (
      <button className={this.context}>Click me</button>
    );
  }
}

const ThemedButtonFC: React.FC<{ theme: string }> = props => (
  <button className={`app-toolbar-button ${props.theme}`}>Click me</button>
);

const Toolbar: React.FC = props => (
  <div>
    Hello, TypeScript & React. <ThemedButton />
  </div>
);

const App: React.FC = () => {
  return (
    <ThemeContext.Provider value="dark"> // (1-2)
      <div className="app">
        <header className="app-header">
          <Toolbar />
        </header>
      </div>
      <div>
        <div>
          <ThemeContext.Consumer>
            {value => <ThemedButtonFC theme={value} />} // (1-3)
          </ThemeContext.Consumer>
        </div>
      </div>
    </ThemeContext.Provider>
  );
};

export default App;

ThemedButtonFC および <ThemeContext.Consumer>...</ThemeContext.Consumer> については、Consumerを使った箇所も確認したかったのでオマケです。

新しいContextでは、React.createContext(defaultValue) を使ってContextオブジェクトを作ります。
コメントで(1) と書いたところは、通常のContextのお作法通りに書いた箇所です。
これだけで、(1-2)Providervalue(1-3)Consumer 内の関数の value は正しく型チェックされるようになります。

ポイントは (2) のところで、どうやらこれがないとコンポーネント内のthis.contextが型チェックされないようです。
この書き方はReactの型定義ファイルを参考にしました。

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L421-L434

 /**
  * If using the new style context, re-declare this in your class to be the
  * `React.ContextType` of your `static contextType`.
  *
  * ```ts
  * static contextType = MyContext
  * context!: React.ContextType<typeof MyContext>
  * ```
  *
  * @deprecated if used without a type annotation, or without static contextType
  * @see https://reactjs.org/docs/legacy-context.html
  */


2. React 16.3 以前の Legacy Context を使う場合

Legacy Context の場合はこのようになります。

import React from "react";
import PropTypes from "prop-types";
import "./App.css";

// (1)
type ThemeContext = {
  theme: string;
};

class ThemedButton extends React.Component {
  static contextTypes = {
    theme: PropTypes.string
  };
  context!: ThemeContext; // (1)

  render() {
    return (
      <button className={this.context.theme}>
        Click me
      </button>
    );
  }
}

const Toolbar: React.FC = props => (
  <div>
    Hello, TypeScript & React. <ThemedButton />
  </div>
);

class App extends React.Component {
  static childContextTypes = {
    theme: PropTypes.string
  };

  getChildContext(): ThemeContext { // (1)
    return { theme: "light" };
  }

  render() {
    return (
      <div className="app">
        <header className="app-header">
          <Toolbar />
        </header>
      </div>
    );
  }
}

export default App;

公式ドキュメント相当の信頼できそうな情報は見つけられなかったのですが、こちらのブログを参考にしました。

ポイントとしては、Legacy Contextの場合、コンテキストはPropTypesで記述するためランタイムでのチェックができません。
そのため、(1) にあるように同じ構造のTypeScriptの型を定義した後、親のgetChildContext()、子のthis.contextに型を付けています。

ブログの記事との違いとして、ブログでは

context: ThemeContext;

としてますが、これだと--strictPropertyInitializationオプションがONのときに以下のエラーが出ます。

Property 'context' has no initializer and is not definitely assigned in the constructor. ts(2564)


3. React Hooksの useContext() を使った場合

最後に、Hooksを使った場合。

import React, { useContext } from "react";
import "./App.css";

const ThemeContext = React.createContext("light");

const ThemedButton: React.FC = props => {
  const theme = useContext(ThemeContext);

  return <button className={theme}>Click me</button>;
};

const Toolbar: React.FC = props => (
  <div>
    Hello, TypeScript & React. <ThemedButton />
  </div>
);

const App: React.FC = () => {
  return (
    <ThemeContext.Provider value="dark">
      <div className="app">
        <header className="app-header">
          <Toolbar />
        </header>
      </div>
    </ThemeContext.Provider>
  );
};

export default App;

コンポーネントが Function Component で書けるようになるということ以外は 1 と変わりません。

Amazonの商品ページの情報をkintoneに登録するChrome拡張

を作りました。

https://chrome.google.com/webstore/detail/amazon-to-kintone/leipfhjipgnfbdjkbinlmlmfdhgcakki からインストールできます。

アイコン未登録だったりコードがぐちゃぐちゃだったりしますが、とりあえず自分が使うための要件は満たせたので一区切り。


キャプチャ

f:id:dackdive:20190520192441p:plain

商品のタイトル、商品ページのURL、画像URLをkintoneの任意のアプリにレコード登録するだけのChrome拡張です。


モチベーション

kintone の勉強がてら、読みたい/読んだ本の記録を kintone でやっています
気になった本はたいていAmazonで検索してkintoneに登録してたんですが、それを1クリックでできるようにしたかった、というのがきっかけ。

また、技術的にもこのへんを試したかったというのもあります。

  • TypeScript で Chrome 拡張を書く
  • React Hooks
  • (未) styled-components

スタイルについてはせっかくなので kintone-ui-component 使って kintone ライクなUIにしようと考えていたけど
もろもろ断念して SalesforceLightning Design System になった。


技術的なメモ

TypeScript

chrome.*** 系のAPIに対する TypeScript の型定義は @types/chrome が利用できる。
Chrome拡張書くときのボイラープレート的なものは今回でだいぶ学べた。

また、 chrome.*** APIを Promise で書けるようにしつつブラウザ間の差異を吸収した webextension-polyfill というのもある。
今回は使わなかったけど興味あるのでリファクタしたい。


Chrome 拡張 ( chrome.*** API まわり)

すごい単純なChrome拡張だったのでPopupのみでいけるかなーと思ってたんだけど、PopupからはページのDOMを触れない。
content scirptを追加してあげる必要がある。

また、content scriptとPopup間のやり取りは chrome.tabs.sendMessage() および chrome.runtime.onMessage.addListener() を使ったメッセージング機能を利用することになる。

// メッセージ送信側
chrome.tabs.sendMessage(props.tabId, { /* パラメータ */ }, response => {
  // 受信側からのレスポンスが返ってきた後の処理
});
// メッセージ受信側
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  // もろもろ処理
  sendResponse(/* レスポンス */);
});

sendRequest() は deprecated
https://developer.chrome.com/extensions/tabs#method-sendRequest


React Hooks

今回は useState, useEffect しか使ってない。
慣れるまで少し時間がかかったけど、関心ごとに処理をまとめられるようになるのでたしかに読みやすいかも、と思った。

使わなかったものだとカスタムフック は興味あるのと useContext あたりは便利だという話を聞くので、ドキュメント読みたい。


リファレンス

TypeScriptでJestを使うときの設定(ts-jest, @types/jestなど)

メモ。
TypeScript を使ったプロジェクトに Jest を導入する時に必要なパッケージや設定、とくに ts-jest@types/jest が必ず必要なのかどうかがよくわかってなかったので調べた。


先にまとめ

TypeScript -> JavaScriptコンパイルを TypeScript 自身でやるか Babel に任せるかで必要な設定が異なる。
Babel 7 から TypeScript がサポートされた

  • @types/jest は(テストファイルも型チェックするなら)両方で必要
  • TypeScript のコンパイルに Babel を使う場合、@preset/typescriptbabel-jest をインストールしておけば ts-jest は不要
  • TypeScript のコンパイルに TypeScript を使う場合、 ts-jest が必要。jest.config.jsonpreset: 'ts-jest' を指定する


1. TypeScript を使った場合の設定

ts-jest 公式ドキュメントに従い、ts-jest@types/jest をインストールする。

https://kulshekhar.github.io/ts-jest/user/install

$ npm install -D jest typescript ts-jest @types/jest

jest.config.jspresetts-jest を指定する。(Configuration > Basic usage より)

// jest.config.js
module.exports = {
  // [...]
  // Replace `ts-jest` with the preset you want to use
  // from the above list
  preset: 'ts-jest'
};


2. Babel を使った場合の設定

jest, typescript, @types/jest が必要なのは 1 と同じ。

$ npm install -D jest typescript @types/jest

それ以外は Jest 公式ドキュメントの Getting Started に記載の通り。
Getting Started · Jest

Babel との併用のために babel-jest @babel/core @babel/preset-env 、さらに TypeScript のために @babel/preset-typescript が必要になる。

$ npm install -D babel-jest @babel/core @babel/preset-env @babel/preset-typescript

そして Babel の設定にインストールした preset を書いておく。

// babel.config.js
module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-typescript',
  ],
};

ドキュメントでは @babel/preset-env{targets: {node: 'current'} を指定しているが、なくても動く。プロジェクトに合わせて設定すればいいはず。

Jest 側の設定(jest.config.json)は特に不要。


注意事項

2 の Babel を使った場合、以下に記載されているように制限事項がいくつかある。

特に Babel でのコンパイルの場合 型チェックをしない というのが大きな差。

たとえばこんなスクリプトを書いててもテストでは無視される。

// main.ts
export function add100(a: number) {
  const b: number = '100'; // 型エラー
  return a + b;
}
// main.spec.ts
import { add100 } from './main';

describe('add100', () => {
  it('should be 1 + 100 = 101', () => {
    expect(add100(1)).toEqual(101);
  });
});
# テスト実行結果。コンパイルエラーでなくアサーションでエラー
$ npm test
 FAIL  src/main.spec.ts
  ● add100 › should be 1 + 100 = 101

    expect(received).toEqual(expected)

    Expected: 101
    Received: "1100"

また、型チェックをしないので @types/jest はインストールしなくてもテストは実行できる。


検証コード

https://github.com/zaki-yama/typescript-jest-setting-sample

認定スクラムマスター研修(CSM)で学んだこと

だいぶ前ですが、2/13-15 に認定スクラムマスター研修を受けて、無事認定スクラムマスターになりました。

f:id:dackdive:20190405022421p:plain:w100

こないだ社内でも報告会をやったんですが、ここにも学んだこととか思ったことをメモしておきます。
研修の内容についての詳細はあまり書かれてません。(別にバラされて困ることない、って講師の方はおっしゃってましたが)


認定スクラムマスター研修 (CSM) について

いくつか運営団体があるようですが、自分が受講したのは Scrum Alliance という非営利団体が管理しているやつです。
Odd-e Japan という会社から申し込みました。

講師はエバッキーこと江端さんで、この方は唯一の日本人の認定スクラムトレーナーです。


学び

スクラムマスターはチームの誰よりも論理的に物事を考えられなければならない

個人的に一番誤解していてショッキングだったのがこれ。
3日間の研修中、本当に論理的思考力を要求されました。

理由は後述。


スクラムとは「プロジェクトの現状把握をするためのフレームワーク
  • 一般にフレームワークとは「課題発見」の手助けとなるものと「課題解決」の手助けとなるもの(あるいはその両方)があるが、スクラムは前者
    • もっと言うと、発見されたものを課題とみなすかどうかまでチームに委ねられている
  • 課題を解決するのはチームのメンバー。スクラムが解決してくれるわけではない

...という話を聞いて、前もネットで似たようなこと聞いたなーと思ったら ryuzee さんのこのツイートでした。


世に出回っている「スクラム」に関する本は、ほぼ「チーム(開発メンバー)」目線で書かれたもの。スクラムマスターの目線で書かれた本はほとんどない
  • スクラムに登場する3つの役割として「プロダクトオーナー」「チーム(開発チーム)」「スクラムマスター」があるが、各役割から見たスクラムは全く別モノ
    • 責任範囲が違うので
    1. じゃあ、スクラムマスター目線でスクラムを学ぶには?
      1. スクラムアジャイル開発が生まれた背景にある学問を学ぶ。「組織論」と「集団心理学」

優れたスクラムマスターというのは、極論言うと「「チームの目的にスクラムがマッチしてなかったらスクラムを辞め、別の手法をとる」ことを提案できないといけない。
そのためにはスクラムがどういう背景で生まれたのかとか、その他の手法について熟知してないといけない。
「何がなんでもスクラムをやる」はスクラムマスターじゃなくてスクラムスレーブ(奴隷)。


スクラムチームの3つの役割と責任
  • プロダクトオーナー
    • 投資対効果(ROI)を最大化する
  • チーム(開発チーム)
  • スクラムマスター
    • プロダクトオーナーやチームが目的を達成する確率を最大化する

※ROI や生産性の定義(何を分母分子とするか)は目的によって異なる

すべて「最大化」なのがポイント。常に過去より良くする活動を続ける責任がある。


2つの責任:Accountability と Responsibility
  • 未来に対する責任が Responsibility(説明責任)、過去に対する責任が Accountability(実行責任)
  • 例:熱々のやかんに手を触れようとしている子供と、その親がいて
    • 親が子に向かって「やけどするから危ないよ!」と注意喚起するのが Responsibility
    • 子がそれでもやかんを触ってやけどしてしまったら、それは子の Accountability
  • スクラムマスターが背負うのは 基本的に Responsibility のみ
    • プロダクトオーナーやチームは Accountability も Responsibility も持つ
  • 実行責任を伴わない分、スクラムマスターがプロダクトオーナーやチームに対して行う提案には非常に重みがある
    • 誰よりも「なぜそうすべきなのか」を論理的に説明できないといけないし、その提案は関係者の次の行動に影響を与えるので大きな責任が伴う
    • かつ、スクラムマスターが行うのはあくまで「提案」であって「指示」ではない。提案を受け入れるかどうかはプロダクトオーナーやチームに委ねられている

なので、スクラムマスターには常に現状から一歩先の未来を見通す能力が必要。チームと同じ時間軸で物事を考えているだけでは不十分。
未来に向かって今取るべきアクションを論理的に考え、他人に説明し、納得してもらった上で対象者の行動を変えていかなければならない。
※何がなんでも自分の意見を採用してもらう、というわけではない。提案を納得してもらった上で、チームが別の選択を取るということも受け入れなければならない。その場合はそこでの選択を受けて事前に描いていた未来を描き直し、次の提案に向けてすぐに動き出さなければならない。


自律的なチームに必要な要素
  • 目的が明確
    • できていたかどうかの判断が人によってぶれない
  • 境界線が明確
    • やっていいことと悪いこと、判断基準がはっきりしている
  • 目的を実現するために何をすべきか瞬時に判断し、行動し続けている


(質問)チームが自律的になったらスクラムマスターは不要ですか?スクラムマスターがいないチームというのは健全でしょうか?

これは研修の最中に自分が講師の方にした質問。
今の自分のチームにはスクラムマスターがいないので、それってアリなの?というのが気になっていた。

回答としては「基本的にスクラムマスターが不要になる瞬間というのはないと思って良い」とのこと。

ただ、続けておっしゃっていたのが

スクラムマスターがいなくなることはない。なぜなら、スクラムマスターが関わる対象は
開発チーム→プロダクトオーナーまで含めたスクラムチーム→複数のスクラムチーム→ステークホルダーも含めた「組織」
まで広げていかないといけないから。
逆に言うと、開発チームというミクロな視点で見てスクラムマスターがいないというのは問題だとは思わない

ということだったので、モヤモヤが晴れたとともにスクラムマスターが背負っていうミッションの大きさを再認識させられました。


感想

研修中は課題に対してとにかく論理的に解を出すことが求められ、ちょっとでも理論に穴があると突っ込まれ、停滞する、ということを繰り返した3日間で、本当にきつかったです。
日頃いかになんとなく行動を決めているかを痛感します。

スクラムマスターというとチームが自律的になるための支援をする人、ぐらいの認識だったので、どちらかというと人間力のようなものが求められるのかと思っていましたが
そこが間違いだったことに気づけたのは個人的には一番の学びでした。

また、重要なのは今後自分がどう行動していくか、また自分の行動によって周囲の行動をどう変えていくかだと思いますが
今のところチームでスクラムマスターになりたいと立候補するつもりはなくて、
スクラムマスターとしての考え方を身につけた上で、引き続きチームの1メンバーとしてカイゼンを続けていこうと思っています。

VSCodeVimでxやsでコピー(ヤンク)しないようにする

メモ。

素の Vim ではこちらの記事を参考に xs で1文字削除したときにクリップボードにコピーされないようにしていて、

VSCodeVim でも同様の設定をしたい。


方法

settings.json (コマンドパレットの Preferences: Open Settings (JSON)) を開き、以下を追記する。

"vim.normalModeKeyBindings": [
  {
    "before": ["x"],
    "after": ["\"", "_", "x"]
  },
  {
    "before": ["s"],
    "after": ["\"", "_", "s"]
  }
]