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
100
Google I/O Extended Japan 2023 - Web Performance at CyberAgent
herablog
0
330
2023年、知っておきたいWebのこと ~フレームワーク・Web UI~ / web-frameworks-and-web-ui-in-2023
herablog
0
1.7k
Enjoy the Web
herablog
5
1.6k
2022年、知っておきたいWebのこと ~パフォーマンス & セキュリティ~
herablog
2
590
Core Web Vitals in Practice
herablog
6
7.3k
Scalable PWA
herablog
7
7.9k
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
エンジニアのキャリアパスと、 その中で自分が大切にしていること
noteinc
4
3.4k
Scala meets WebAssembly
tanishiking
0
120
OSSの実装を参考にBedrockエージェントを作る
moritalous
2
430
マーケットプレイス版Oracle WebCenter Content For OCI
oracle4engineer
PRO
3
560
x86-64 Assembly Essentials
latte72
4
1k
ライフステージの変化を乗り越える 探索型のキャリア選択
tenshoku_draft
2
550
【Snowflake九州ユーザー会#2】BigQueryとSnowflakeを比較してそれぞれの良し悪しを掴む / BigQuery vs Snowflake: Pros & Cons
civitaspo
5
1.7k
Amazon Bedrock Knowledge basesにLangfuse導入してみた
sonoda_mj
2
390
MLflowはどのようにLLMOpsの課題を解決するのか
taka_aki
0
190
完璧を捨てろ! “攻め”のQAがもたらすスピードと革新/20250306 Hiroki Hachisuka
shift_evolve
0
190
開発者のための FinOps/FinOps for Engineers
oracle4engineer
PRO
2
310
エンジニア採用と 技術広報の実践/acaricsummit2025
nishiuma
1
160
Featured
See All Featured
Building Adaptive Systems
keathley
40
2.4k
Producing Creativity
orderedlist
PRO
344
40k
Done Done
chrislema
182
16k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
656
59k
Fashionably flexible responsive web design (full day workshop)
malarkey
406
66k
The Art of Programming - Codeland 2020
erikaheidi
53
13k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
PRO
13
1k
Visualization
eitanlees
146
15k
Principles of Awesome APIs and How to Build Them.
keavy
126
17k
How STYLIGHT went responsive
nonsquared
99
5.4k
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
120k
Design and Strategy: How to Deal with People Who Don’t "Get" Design
morganepeng
129
19k
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/