Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥
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
7
4.8k
こえのブログでの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
140
Google I/O Extended Japan 2023 - Web Performance at CyberAgent
herablog
0
390
2023年、知っておきたいWebのこと ~フレームワーク・Web UI~ / web-frameworks-and-web-ui-in-2023
herablog
0
1.8k
Enjoy the Web
herablog
5
1.6k
2022年、知っておきたいWebのこと ~パフォーマンス & セキュリティ~
herablog
3
620
Core Web Vitals in Practice
herablog
6
8.5k
Scalable PWA
herablog
7
9.3k
CDNフル活用でつくる、高速Webアプリ / Using CDN To Improve Web Performance
herablog
15
8.3k
Web App Checklist 〜高品質のWebアプリケーションをつくるために〜 / Web App Checklist 2019 at Inside Frontend
herablog
23
11k
Other Decks in Technology
See All in Technology
5分で知るMicrosoft Ignite
taiponrock
PRO
0
360
AWS CLIの新しい認証情報設定方法aws loginコマンドの実態
wkm2
6
740
業務のトイルをバスターせよ 〜AI時代の生存戦略〜
staka121
PRO
2
180
チーリンについて
hirotomotaguchi
6
1.9k
EM歴1年10ヶ月のぼくがぶち当たった苦悩とこれからへ向けて
maaaato
0
280
今からでも間に合う!速習Devin入門とその活用方法
ismk
1
700
Edge AI Performance on Zephyr Pico vs. Pico 2
iotengineer22
0
150
コンテキスト情報を活用し個社最適化されたAI Agentを実現する4つのポイント
kworkdev
PRO
0
1.2k
プロンプトやエージェントを自動的に作る方法
shibuiwilliam
6
4.8k
WordPress は終わったのか ~今のWordPress の制作手法ってなにがあんねん?~ / Is WordPress Over? How We Build with WordPress Today
tbshiki
1
760
意外とあった SQL Server 関連アップデート + Database Savings Plans
stknohg
PRO
0
320
Power of Kiro : あなたの㌔はパワステ搭載ですか?
r3_yamauchi
PRO
0
130
Featured
See All Featured
[RailsConf 2023] Rails as a piece of cake
palkan
58
6.2k
How STYLIGHT went responsive
nonsquared
100
6k
Improving Core Web Vitals using Speculation Rules API
sergeychernyshev
21
1.3k
For a Future-Friendly Web
brad_frost
180
10k
Designing Dashboards & Data Visualisations in Web Apps
destraynor
231
54k
Navigating Team Friction
lara
191
16k
Keith and Marios Guide to Fast Websites
keithpitt
413
23k
Facilitating Awesome Meetings
lara
57
6.7k
The Cost Of JavaScript in 2023
addyosmani
55
9.3k
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
508
140k
Documentation Writing (for coders)
carmenintech
76
5.2k
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
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/