@mittlmedien
Joomla 4 in Stream?
info@mittl-medien.de
About me
- Robert Mittl
- Raum Stuttgart
- Web Entwickler
- PHP, JavaScript
- Progressive Web App (PWA)
- CMS Joomla
- Creator of miTT PWA (PWA Joomla Plugin)
📱Pain
- Performance
- Request
- First Byte
Service Worker
- Proxy
- Requests werden kontrolliert
- Cache Api
Service Worker können mehr als nur Caching 🤗
Stream API
- Javascript Api
- Split to Chunks
Kann die Stream-API für ein CMS wie Joomla funktionieren?
Funktionsweise
- Chunks (kleine Datenpakete)
- Readable Streams
- Writeable Streams
- Transform Streams
- Pipe Chains (PipeTo(), PipeThrough()
- Push und Pull sources
streame das Joomla Logo oder doch besser eine
🐢
import GrayscalePNGTransformer from './png-lib';
(async () => {
const image = document.getElementById('target');
// Fetch the original image
const response = await fetch('turtle.png');
// Retrieve its body as ReadableStream
const body = response.body;
// Create a gray-scaled PNG stream out of the original
const rs = body.pipeThrough(new TransformStream(new GrayscalePNGTransformer()));
// Create a new response out of the stream
const newResponse = new Response(rs);
// Create an object URL for the response
const blob = await newResponse.blob();
// create URL
const url = URL.createObjectURL(blob);
// Update image
image.src = url;
})();
mehr Beispiele: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
mehr Beispiele: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
Web - Server
Service Worker
Service Worker
📱
Cache
Cache API
Stream API with Service Worker
App Shell
Cache HEAD und Header
Cache Footer
STREAM
App Shell
Webpage
- Laden der Shell aus dem Cache
- vermeiden blockieren von JS, CSS...
window.addEventListener('load', async() => {
if("serviceWorker" in window.navigator) {
try {
await window.navigator.serviceWorker.register('/serviceworker.js')
} catch (error) {
console.error(error)
}
}
})
Service Worker registration
im Template z.B. index.php
Service Worker Lifecycle 🔄
-> Registration
-> Installation
-> Wait
-> Activate
Service Worker Installation
statischen Cache hinzufügen
im Service Worker service worker.js
addEventListener('install', event => {
skipWaiting();
event.waitUntil(async function () {
const cache = await caches.open(staticCacheName);
await cache.addAll(toCache);
}());
});
Cache managen
im Service Worker
addEventListener('activate', event => {
event.waitUntil(async function () {
const keys = await caches.keys();
await Promise.all(
keys.map(key => {
if (key !== staticCacheName) return caches.delete(key);
})
);
}());
});
Ausgabe im Service Worker
event.respondWith(async function() {
if (event.request.method !== 'GET') return;
const cachedResponse = await caches.match(url)
if (cachedReponse) return cachedResponse;
// Stream you content
//..........
})
Start with Streaming
const readableStream = new ReadableStream({
start(controller) {
/* … */
},
pull(controller) {
/* … */
},
cancel(reason) {
/* … */
},
});
async function streamArticle(request, url) {
const stream = new ReadableStream({
start(controller) {
const startFetch = caches.match('/media/plg_system_mittpwa/header.html');
const endFetch = caches.match('/media/plg_system_mittpwa/footer.html');
const middleFetch = fetch(url).then(response => {
if (!response.ok && response.status != 404) {
return caches.match('/media/plg_mittpwa/error.html');
}
return response;
}).catch(err => caches.match('/media/plg_system_mittpwa/offline.html'));
function pushStream(stream) {
const reader = stream.getReader();
return reader.read().then(function process(result) {
if (result.done) return;
controller.enqueue(result.value);
return reader.read().then(process);
});
}
startFetch
.then(response => pushStream(response.body))
.then(() => middleFetch)
.then(response => pushStream(response.body))
.then(() => endFetch
.then(response => pushStream(response.body))
.then(() => controller.close());
}
});
return new Response(stream, {
headers: {'Content-Type': 'text/html; charset=utf-8'}
});
}
addEventListener('fetch', event => {
let url = new URL(event.request.url);
event.respondWith(async function() {
//.....Cached content
let parts
parts = [
caches.match('/media/plg_system_mittpwa/header.include'),
fetch(url)
.catch(() => caches.match('/media/plg_system_mittpwa/offline.include')),
caches.match('/media/plg_system_mittpwa/footer.include')
];
const {done, response} = await mergeResponses(parts);
event.waitUntil(done);
// Return the merged response.
return response;
}());
});
RespondWith - Ausgabe
im Service Worker
async function mergeResponses(responsePromises, headers) {
const readers = responsePromises.map(p => Promise.resolve(p).then(r => r.body.getReader()));
let doneResolve;
let doneReject;
const done = new Promise((r, rr) => {
doneResolve = r;
doneReject = rr;
});
const readable = new ReadableStream({
async pull(controller) {
const reader = await readers[0];
try {
const {done, value} = await reader.read();
if (done) {
readers.shift();
if (!readers[0]) {
controller.close();
doneResolve();
return;
}
return this.pull(controller);
}
controller.enqueue(value);
}
catch (err) {
doneReject(err);
throw err;
}
},
cancel() {
doneResolve();
}
});
Merged Response
async function mergeResponses(responsePromises, headers) {
const {readable, writable} = new TransformStream();
const done = (async function() {
for await (const response of responsePromises) {
await response.body.pipeTo(writable, {preventClose: true});
}
writable.getWriter().close();
})();
return {
done,
response: new Response(readable, {
headers: headers || (await responsePromises[0]).headers
})
};
}
Merged Response
Navigation Preload
- längere Ladezeit, wenn der Service Worker noch hochgefahren werden muss
- der Browser wartet auf den Fetch Request
- Abhilfe Navigation Preload
- erlaubt paralleles Laden
SW Boot
SW Boot
Navigation Request
Navigation Request
addEventListener('activate', event => {
event.waitUntil(async function() {
// Feature-detect
if (self.registration.navigationPreload) {
// Enable navigation preloads!
await self.registration.navigationPreload.enable();
}
}());
});
Activation
Navigation Preload
addEventListener('fetch', event => {
event.respondWith(async function() {
// Respond from the cache if we can
const cachedResponse = await caches.match(event.request);
if (cachedResponse) return cachedResponse;
// Else, use the preloaded response, if it's there
const response = await event.preloadResponse;
if (response) return response;
// Else try the network.
return fetch(event.request);
}());
});
Response
Navigation Preload
// Try to use the preload
const networkContent = Promise.resolve(event.preloadResponse)
// Else do a normal fetch
.then(r => r || fetch(includeURL))
// A fallback if the network fails.
.catch(() => caches.match('/article-offline.include'));
const parts = [
caches.match('/article-top.include'),
networkContent,
caches.match('/article-bottom')
];
Navigation Preload
Joomla 4 🤔
Möglichkeit Template
- Template Umbau
- Nutzung der Ajax Schnittstelle für den fetch request
- onAjaxMeinPlugin()
- ?option=com_ajax&meinmodule
- ?option=com_ajax&meinplugin
- getHeadData()
- Joomla\CMS\Document\Renderer\Html\HeadRenderer->render()
Möglichkeit Joomla API
- Head selbst bauen
- Content und Module über die API abrufen
💃🏻 Demo 🕺🏻
Quellen - nützliche Links:
web.dev/stream | Peter Steiner
MDN Web Docs Stream
Jake Archibald | Gist | YouTube
slides.com/robertmittl
Questions? 🧐
📬 info@mittl-medien.de
Twitter: @mittlmedien
Thank You 😅
Stream Joomla 4?
By Robert Mittl
Stream Joomla 4?
- 431