@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;
})();

 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