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.4k
こえのブログでの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
69
Google I/O Extended Japan 2023 - Web Performance at CyberAgent
herablog
0
280
2023年、知っておきたいWebのこと ~フレームワーク・Web UI~ / web-frameworks-and-web-ui-in-2023
herablog
0
1.4k
Enjoy the Web
herablog
5
1.5k
2022年、知っておきたいWebのこと ~パフォーマンス & セキュリティ~
herablog
2
560
Core Web Vitals in Practice
herablog
6
6.6k
Scalable PWA
herablog
7
7k
CDNフル活用でつくる、高速Webアプリ / Using CDN To Improve Web Performance
herablog
15
8.1k
Web App Checklist 〜高品質のWebアプリケーションをつくるために〜 / Web App Checklist 2019 at Inside Frontend
herablog
23
10k
Other Decks in Technology
See All in Technology
Discovering AI Models
picardparis
4
3.9k
Tricentisにおけるテスト自動化へのAI活用ご紹介/20240910Shunsuke Katakura
shift_evolve
0
180
Creative UIs with Compose: DroidKaigi 2024
chrishorner
1
480
事前準備が肝!AI活用のための業務改革
layerx
PRO
1
370
Google CloudのLLM活用の選択肢を広げるVertex AIのパートナーモデル
nayuts
0
130
o1のAPIで実験してみたが 制限きつすぎて辛かった話
pharma_x_tech
0
120
自社サービスのための独自リリース版Redmine「RedMica」の取り組み
vividtone
0
1.3k
突撃! 隣のAmazon Bedrockユーザー 〜YouはどうしてAWSで?〜
minorun365
PRO
3
380
どこよりも遅めなWinActor Ver.7.5.0 新機能紹介
tamai_63
0
200
自作Cコンパイラ 8時間の奮闘
soukouki
0
830
Next.js のページ遷移を全力で止める
ypresto
3
1.6k
「自動テストのプラクティスを効果的に学ぶためのカードゲーム」 ( #sqip2024 )
teyamagu
PRO
2
180
Featured
See All Featured
How to name files
jennybc
75
98k
Faster Mobile Websites
deanohume
304
30k
What's in a price? How to price your products and services
michaelherold
242
11k
Debugging Ruby Performance
tmm1
72
12k
Visualization
eitanlees
142
15k
What's new in Ruby 2.0
geeforr
340
31k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
157
15k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
190
16k
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
354
29k
Mobile First: as difficult as doing things right
swwweet
221
8.8k
Building an army of robots
kneath
302
42k
Raft: Consensus for Rubyists
vanstee
135
6.5k
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/