Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
こえのブログでのPWA ~ PWA編 ~ / PWA Night Vol.4
Search
Kazunari Hara
May 15, 2019
Technology
8
4.5k
こえのブログでのPWA ~ PWA編 ~ / PWA Night Vol.4
PWA Night vol.4 ~PWAのミライや活用方法をみんなで考えよう~の資料です。
https://pwanight.connpass.com/event/128434/
Kazunari Hara
May 15, 2019
Tweet
Share
More Decks by Kazunari Hara
See All by Kazunari Hara
Amebaデザインシステム Spindleの開発 / The Development of Spindle
herablog
2
84
Google I/O Extended Japan 2023 - Web Performance at CyberAgent
herablog
0
310
2023年、知っておきたいWebのこと ~フレームワーク・Web UI~ / web-frameworks-and-web-ui-in-2023
herablog
0
1.6k
Enjoy the Web
herablog
5
1.5k
2022年、知っておきたいWebのこと ~パフォーマンス & セキュリティ~
herablog
2
570
Core Web Vitals in Practice
herablog
6
7k
Scalable PWA
herablog
7
7.5k
CDNフル活用でつくる、高速Webアプリ / Using CDN To Improve Web Performance
herablog
15
8.2k
Web App Checklist 〜高品質のWebアプリケーションをつくるために〜 / Web App Checklist 2019 at Inside Frontend
herablog
23
10k
Other Decks in Technology
See All in Technology
ゼロから創る横断SREチーム 挑戦と進化の軌跡
rvirus0817
2
260
生成AIのガバナンスの全体像と現実解
fnifni
1
180
KubeCon NA 2024 Recap: How to Move from Ingress to Gateway API with Minimal Hassle
ysakotch
0
200
レンジャーシステムズ | 会社紹介(採用ピッチ)
rssytems
0
150
kargoの魅力について伝える
magisystem0408
0
200
DevOps視点でAWS re:invent2024の新サービス・アプデを振り返ってみた
oshanqq
0
180
祝!Iceberg祭開幕!re:Invent 2024データレイク関連アップデート10分総ざらい
kniino
2
250
フロントエンド設計にモブ設計を導入してみた / 20241212_cloudsign_TechFrontMeetup
bengo4com
0
1.9k
Qiita埋め込み用スライド
naoki_0531
0
860
あの日俺達が夢見たサーバレスアーキテクチャ/the-serverless-architecture-we-dreamed-of
tomoki10
0
430
Fanstaの1年を大解剖! 一人SREはどこまでできるのか!?
syossan27
2
160
サイボウズフロントエンドエキスパートチームについて / FrontendExpert Team
cybozuinsideout
PRO
5
38k
Featured
See All Featured
Testing 201, or: Great Expectations
jmmastey
40
7.1k
StorybookのUI Testing Handbookを読んだ
zakiyama
27
5.3k
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
48
2.2k
What’s in a name? Adding method to the madness
productmarketing
PRO
22
3.2k
The World Runs on Bad Software
bkeepers
PRO
65
11k
The Pragmatic Product Professional
lauravandoore
32
6.3k
Building Your Own Lightsaber
phodgson
103
6.1k
It's Worth the Effort
3n
183
28k
How to Think Like a Performance Engineer
csswizardry
22
1.2k
Optimising Largest Contentful Paint
csswizardry
33
3k
How to train your dragon (web standard)
notwaldorf
88
5.7k
Scaling GitHub
holman
458
140k
Transcript
こえのブログでのPWA ~ PWA編 ~ 2019年5月15日 @株式会社ウフル Kazunari Hara
原 一成 Hara Kazunari Web Developer @herablog
None
None
喋るだけで ブログになる
本人の”声”でコンテンツ価値向上 https://voice.ameba.jp/emb ed/kobayashi-maya/rxxqHm 6s4iAqYRP4mjK5 https://voice.ameba.jp/e mbed/kose-sports/eaxzb 5mP6vMlw3FWpqX9 https://voice.ameba.jp/e mbed/toshl-official/9nzC7 iAFn6IDKHPerj6P
None
https://github.com/webmaxru/progressive-web-apps-logo
None
None
None
None
None
クロスプラットフォーム 小さくリリースできる ブラウザ機能の充実 ✖
Lighthouseでhttps://voice.ameba.jp/をMobile、Simulated Fast 3G、4x CPU Slowdown、ローカル環境で測定。
are user experiences.” “ https://developers.google.com/web/progressive-web-apps/
キャッシュ UIパーツ マルチメディア
サーバーサイド & クライアントサイド キャッシュ
Network GET / GET /voice-app.js GET /api/entry.json
Server DB Browser I/O 計算量 キャパシティ リダイレクト クエリ性能 ネットワーク状況 地理
Server DB Browser CDN
CDN利用: できる限りキャッシュ イベント駆動パージ エッジコンピューティング
できる限りキャッシュ: Time To Live (TTL) Surrogate Key
Method Path TTL Surrogate Key GET / max-age=2592000 web, web/release
GET /src/components/voice-app.js max-age=2592000 web, web/release GET /assets/audios/stadard/$USER _ID/$ENTRY_ID.mp3 max-age=2592000 api, entry/$ENTRY_ID, blogger/$USER_ID GET /api/entries/$USER_ID/$ENTR Y_ID/ max-age=2592000 api, entry/$ENTRY_ID, blogger/$USER_ID GET /api/playcounts/$USER_ID/$EN TRY_ID/ max-age=30, stale-while-revali date=120 api, entry/$ENTRY_ID, blogger/$USER_ID
イベント駆動パージ
# Surrogate Keyを操作 sub vcl_fetch { declare local var.SurrogateKey STRING;
If (req.http.x-url ~ "/audios/standard/([a-z0-9-]{3,24})/([a-zA-Z0-9]+)") { set var.SurrogateKey = var.SurrogateKey + " blogger/" + re.group.1 + " entry/" + re.group.2 + " audio/" + re.group.2; // e.g. "blogger/abcde", "entry/12345" } set beresp.http.Surrogate-Key = var.SurrogateKey; }
# ブラウザに配信するHTTPレスポンスヘッダーを追加 sub vcl_deliver { add resp.http.Server-Timing = fastly_info.state {",
fastly;desc="Edge time";dur="} time.elapsed.msec; set resp.http.Referrer-Policy = "origin-when-cross-origin"; set resp.http.X-Content-Type-Options = "nosniff"; add resp.http.Content-Security-Policy = "default-src 'self'; script-src 'self'..." }
CDN詳細は、 WEB+DB PRESS vol.109
クライアントキャッシュ: HTTP Headers Service Worker (Cache API) キャッシュ
HTTP Headerでの キャッシュ Cache-Control: maxage=3600
“25.5% of all logged requests were missing the cache.” https://code.fb.com/web/web-performance-cache-efficiency-exercise/
Service Worker (Cache API) で オリジン毎にキャッシュコ ントロール
プリキャッシュ: アプリの雛形となる ファイル(HTML, Image, JS) 全て Index.html (Entrypoint) Voice-app.js (App
shell) voice-home.js voice-editor.js lazy-resources.js PRPLパターン (Fragment)
プリキャッシュ: 各ファイルの変更毎に 入れ替え workbox.precaching.precacheAndRou te([{ url: "index.html", revision: "999s0cnacavav" },
…]);
index.html
onload Service Worker
Pre-cache assets
Reload Activate Service Worker
No Network Connection
Update Service Worker New Version App
変更があるファイル だけ更新
No Network Connection
ランタイムキャッシュ: オフラインや次回訪問に 備えてAPIデータや アセットをキャッシュ Cache First アートワーク画像 Network First 変更が多いAPIデータ
Stale While Revalidate 変更が少ないAPIデータ
None
UIパーツ
Web Components (LitElement) CSRのWebアプリ モバイルター ゲット コンポーネント Web標準 Web Components
<!-- use component --> <voice-mic recording> </voice-mic>
class VoiceMic extends LitElement { _handleMicClick() { this.dispatchEvent(new CustomEvent('mic-click')); }
render() { const { disabled, recording } = this; const buttonLabel = recording ? '停止' : '開始'; return html` <style></style> <button aria-label=${buttonLabel} aria-live="assertive" ?disabled=${disabled} type="button" @click=${this._handleMicClick} ></button> `; } } customElements.define('voice-mic', VoiceMic);
class VoiceMic extends LitElement { _handleMicClick() { this.dispatchEvent(new CustomEvent('mic-click')); }
render() { const { disabled, recording } = this; const buttonLabel = recording ? '停止' : '開始'; return html` <style></style> <button aria-label=${buttonLabel} aria-live="assertive" ?disabled=${disabled} type="button" @click=${this._handleMicClick} ></button> `; } } customElements.define('voice-mic', VoiceMic);
class VoiceMic extends LitElement { _handleMicClick() { this.dispatchEvent(new CustomEvent('mic-click')); }
render() { const { disabled, recording } = this; const buttonLabel = recording ? '停止' : '開始'; return html` <style></style> <button aria-label=${buttonLabel} aria-live="assertive" ?disabled=${disabled} type="button" @click=${this._handleMicClick} ></button> `; } } customElements.define('voice-mic', VoiceMic);
class VoiceMic extends LitElement { _handleMicClick() { this.dispatchEvent(new CustomEvent('mic-click')); }
render() { const { disabled, recording } = this; const buttonLabel = recording ? '停止' : '開始'; return html` <style></style> <button aria-label=${buttonLabel} aria-live="assertive" ?disabled=${disabled} type="button" @click=${this._handleMicClick} ></button> `; } } customElements.define('voice-mic', VoiceMic);
class VoiceMic extends LitElement { _handleMicClick() { this.dispatchEvent(new CustomEvent('mic-click')); }
render() { const { disabled, recording } = this; const buttonLabel = recording ? '停止' : '開始'; return html` <style></style> <button aria-label=${buttonLabel} aria-live="assertive" ?disabled=${disabled} type="button" @click=${this._handleMicClick} ></button> `; } } customElements.define('voice-mic', VoiceMic);
166 KB (gzip, style込み)
オフライン対応: IndexedDB Service Worker (Cache API) navigator.onLine
下書き保存 with IndexedDB
記事データが 更新されると Indexed DBに 保存
Offline Recording with Service Worker
Index.html (Entrypoint) Voice-app.js (App shell) voice-home.js voice-editor.js lazy-resources.js PRPLパターン (Fragment)
Offline Recording with Service Worker
Offline Notification with navigator.onLine
function watchOffline(callback) { window.addEventListener( ’online’, () => callback(false), ); window.addEventListener(
‘offline’, () => callback(true), ); callback( navigator.onLine === false ); }
watchOffline((offline) => { If (offline) { // Display snack bar
} });
Web App: Web App Manifest Media Queries Responsive Images Desktop
PWA
{ "short_name": "こえ", "name": "こえのブログ by Ameba", "description": "「こえのブログ」は、...", "lang":
"ja-JP", "icons": [], "background_color": "#fff", "theme_color": "#fff", "start_url": "/?source=homescreen", "scope": "/", "display": "standalone" } manifest.json
https://twitter.com/Nkzn/status/1110369084166692864
1025pxからDesktop版
@media screen and (min-width: 1025px) { :root { --app-header-height: 60px;
--app-page-background-color: var(--clr-whitesmoke); --app-drawer-width: 400px; ... } }
Responsive Images <img height="80" width="80" src="toshi.jpg?size=80" srcset=" toshi.jpg?size=160 2x, toshi.jpg?size=240
3x, " />
追加設定なしで Desktop PWA
ネットワーク状況 Network Information API
function watchNetwork (callback) { const connection = navigator.connection; if (connection)
{ callback(connection); connection.addEventListener( 'change', () => callback(connection) ); } } watchNetwork(({ effectiveType } => { if (effectiveType.includes('2g')) { // Display notification } });
Lazy-loading: Intersection Observer Native lazy-loading (onscroll)
Lazy-loading: Intersection Observer Native lazy-loading (Beta)
class LazyloadImage extends LitElement { firstUpdated() { If ('loading' in
HTMLImageElement.prototype) { this.shadowRoot.querySelector(‘img’).src = this.src; } else { // Use Intersection Observer } } render() { return html` <img alt=${alt} data-src=${src} loading="lazy" height=${height} srcset=${srcset} width=${width} /> `; } }
こえのブログをシェア with Web Share API
if (navigator.share) { navigator.share({ title: ‘こえのブログ by Ameba’, text: ‘こえのブログは...’,
url: ‘https://voice.ameba.jp/’, }); } else { // Open custom dialog }
こえのブログを貼り付け with Clipboard API
const text = ‘text to copy’; if (navigator.clipboard) { navigator.clipboard.writeText(
Text ); } else { // document.execCommand('copy'); }
Vibrate Notification with Vibration API
ブッ ブブ ブゥ 録音開始 残り10秒 録音終了
function notifyRecordingStart() { navigator.vibrate(30); } function notifyTimeToFinish() { navigator.vibrate([30, 100,
30]); } function notifyRecordingEnd() { navigator.vibrate(100); } ブブ
マルチメディア
Audio Recording
Mic Web Worker Browser Blob Stream Messaging Messaging
端末のマイクに アクセス navigator.mediaDevices .getUserMedia({ audio: { autoGainControl: false, channelCount: 1,
echoCancellation: true, noiseSuppression: true, }, }) .then((stream) => { // use the stream }) .catch((err) => { // NotAllowedError or // NotFoundError });
録音中の音声圧縮 WebAssembly & Web Worker WAV MP3
https://github.com/Kagami/vmsg Kagami/vmsg
録音した音声を操作 with Blob (Binary Large OBject) // Save to IndexedDB
const transaction = db.transaction( ['voice'], 'readwrite'); const objectStore = transaction.objectStore('voice'); const objectStoreRequest = objectStore.put({ audio: blob }); // Create URL to play audio URL.createObjectURL(blob); blob:https://voice.ameba.jp/76ee6fef-c126-4 f83-8a46-8fb00db57808
Read Photo
Camera/Photo Browser Server Blob ArrayBuffer File API Resize/Upload
<input type="file" accept="image/jpeg" /> function onFileChange(event) { const el =
event.target; if (el.files && el.files[0]) { const reader = new FileReader(); reader.onload = e => { const buffer = e.target.result; const type = el.files[0].type; const blob = new Blob([buffer], { type }); const fileName = el.files[0].name; }; } } 端末画像を読み込み with File API
window.loadImage( blob, canvas = > canvas.toBlob( resizedBlob => { //
Display resized image } ), { canvas: true, maxHeight: 1024px, maxWidth: 1024px, }, ); アップロード前に リサイズ
https://github.com/blueimp/JavaScript-Load-Image blueimp/ JavaScript-Load-Image (将来的に変更する可能性あり)
https://developers.cyberagent.co.jp/blog/archives/20506/ 詳細は・・・ CDN/PWA/Speech Recognition/WASM/Web Components/Service Worker/Performance Budget etc... https://speakerdeck.com/herablog/koe-no-blog-pwa こえのブログでのPWA
are user experiences.” “ https://developers.google.com/web/progressive-web-apps/