Gestione della complessità

Mantenere un'app web semplice può essere sorprendentemente complicato. In questo modulo imparerai come le API web funzionano con il thread e come puoi utilizzarlo per i pattern PWA più comuni come la gestione dello stato.

Nel suo discorso Simple Made Easy, Rich Hickey parla della qualità delle cose semplici e delle cose complesse. Descrive elementi semplici come:

"Un ruolo, un'attività, un concetto o una dimensione."

ma sottolinea che la semplicità non riguarda:

"Avere un'istanza o eseguire un'operazione."

Che le cose semplici siano o meno semplici dipendono dalla loro interconnessione.

La complessità deriva dal legame, dall'intrecciatura o, per usare il termine di Rich, dalla combinazione. Puoi calcolare la complessità contando il numero di ruoli, attività, concetti o dimensioni che qualcosa gestisce.

La semplicità è essenziale nello sviluppo del software perché un codice semplice è più facile da capire e gestire. La semplicità è necessaria anche per le app web, perché possono contribuire a rendere l'app veloce e accessibile in ogni contesto possibile.

Gestione della complessità delle PWA

Tutto il codice JavaScript che scriviamo per il web tocca il thread principale a un certo punto. Il thread principale, tuttavia, presenta una grande complessità preconfigurata su cui gli sviluppatori non hanno alcun controllo.

Il thread principale è:

  • È responsabile del disegno della pagina, che a sua volta è un complesso processo in più fasi che implica il calcolo degli stili, l'aggiornamento e la composizione dei livelli e la colorazione su schermo.
  • È responsabile di ascoltare e reagire agli eventi, inclusi eventi come lo scorrimento.
  • Responsabile del caricamento e dello scarico della pagina.
  • Gestione di contenuti multimediali, sicurezza e identità. Tutto questo prima che qualsiasi codice che scrivi possa essere eseguito nel thread, come ad esempio:
  • Manipolazione del DOM.
  • Accesso alle API sensibili, ad esempio funzionalità del dispositivo o contenuti multimediali/sicurezza/identità.

Come ha affermato Surma nel suo discorso sul Chrome Dev Summit 2019, il thread principale è oberato e sottopagato.

Eppure, la maggior parte del codice dell'applicazione risiede anche nel thread principale.

Tutto quel codice si aggiunge alla complessità del thread principale. Il thread principale è l'unico che il browser può utilizzare per disporre e visualizzare i contenuti sullo schermo. Pertanto, quando il codice richiede una potenza di elaborazione sempre maggiore, dobbiamo eseguirlo rapidamente, poiché ogni secondo necessario per eseguire la logica dell'applicazione equivale a un secondo in cui il browser non può rispondere all'input dell'utente o ritracciare la pagina.

Quando le interazioni non si connettono all'input, quando i frame cadono o quando occorre troppo tempo per utilizzare un sito, gli utenti si sentono frustrati, sentono che l'applicazione non funziona e la loro fiducia in quest'ultima diminuisce.

La cattiva notizia? Aggiungere complessità al thread principale è un modo quasi sicuro per rendere difficile il raggiungimento di questi obiettivi. La buona notizia? Perché è chiaro ciò che deve fare il thread principale: può essere utilizzato come guida per ridurre il ricorso a questo thread per il resto dell'applicazione.

Separazioni dei problemi

Esistono molti tipi di attività svolte dalle applicazioni web, ma in generale puoi suddividerle in attività che toccano direttamente l'interfaccia utente e non funzionano. L'utilizzo dell'interfaccia utente prevede che:

  • Tocca direttamente il DOM.
  • Utilizza API che toccano le funzionalità del dispositivo, ad esempio notifiche o accesso al file system.
  • Tocca l'identità, ad esempio i cookie dell'utente, lo spazio di archiviazione locale o di sessione.
  • Gestisce i contenuti multimediali, ad esempio immagini, audio o video.
  • Presenta implicazioni di sicurezza che richiedono l'approvazione dell'utente, ad esempio l'API web seriale.

Le attività non UI possono includere:

  • Calcoli puri.
  • Accesso ai dati (recupero, IndexedDB e così via).
  • Criptovalute.
  • Messaggistica.
  • Creazione o manipolazione di BLOB o stream.

Il lavoro non basato sulla UI è spesso prenotato da lavori dell'interfaccia utente: un utente fa clic su un pulsante che attiva una richiesta di rete per un'API che restituisce risultati analizzati che vengono poi utilizzati per aggiornare il DOM. Quando si scrive codice, spesso si prende in considerazione questa esperienza end-to-end, ma dove ogni parte di quel flusso di solito no. I confini tra il lavoro nell'interfaccia utente e quello non nell'interfaccia utente sono importanti quanto le esperienze end-to-end, poiché sono il primo posto in cui è possibile ridurre la complessità dei thread principali.

Concentrati su una singola attività

Uno dei modi più semplici per semplificare il codice è suddividere le funzioni in modo che ciascuna si concentri su una singola attività. Le attività possono essere determinate dai confini identificati camminando attraverso l'esperienza end-to-end:

  • Per prima cosa, rispondi all'input dell'utente. Questo è un lavoro dell'interfaccia utente.
  • Quindi, crea una richiesta API. Non si tratta di lavoro nell'interfaccia utente.
  • Successivamente, analizza la richiesta API. Anche in questo caso si tratta di un lavoro non UI.
  • Successivamente, determina le modifiche al DOM. Potrebbe trattarsi di un funzionamento dell'interfaccia utente oppure, se stai utilizzando un'implementazione DOM virtuale, potrebbe non essere un funzionamento dell'interfaccia utente.
  • Infine, apporta le modifiche al DOM. Questo è un lavoro dell'interfaccia utente.

I primi confini chiari sono tra il lavoro nell'interfaccia utente e quello non dell'interfaccia utente. A questo punto, è necessario effettuare chiamate di giudizio: l'esecuzione e l'analisi di una richiesta API è una o due attività? Se le modifiche nel DOM non riguardano l'interfaccia utente, vengono integrate nell'API? Nello stesso thread? In un altro thread? Il giusto livello di separazione qui è fondamentale per semplificare il codebase e per essere in grado di spostarne correttamente le parti dal thread principale.

Componibilità

Per suddividere i flussi di lavoro end-to-end di grandi dimensioni in parti più piccole, devi pensare alla componibilità del codebase. Prendi spunto dalla programmazione funzionale e prendi in considerazione quanto segue:

  • Categorizzazione dei tipi di operazioni svolte dall'applicazione.
  • Creazione di interfacce di input e di output comuni.

Ad esempio, tutte le attività di recupero dell'API eseguono l'endpoint API e restituiscono un array di oggetti standard, mentre tutte le funzioni di elaborazione dati acquisiscono e restituiscono un array di oggetti standard.

JavaScript ha un algoritmo di clone strutturato destinato alla copia di oggetti JavaScript complessi. I web worker lo utilizzano per l'invio di messaggi, mentre IndexedDB lo utilizza per archiviare gli oggetti. La scelta delle interfacce da utilizzare con l'algoritmo di clonazione strutturata ne renderà ancora più flessibile l'esecuzione.

Tenendo conto di questo, puoi creare una libreria di funzionalità componibili classificando il codice e creando interfacce di I/O comuni per quelle categorie. Il codice componibile è un tratto distintivo di semplici codebase: pezzi a basso accoppiamento e intercambiabili che possono trovarsi "uno accanto all'altro" e crearsi l'uno sull'altro, a differenza di un codice complesso che è profondamente interconnesso e pertanto non può essere facilmente separato. Sul web, il codice componibile può fare la differenza tra un utilizzo eccessivo o meno del thread principale.

Con il codice componibile a portata di mano, è il momento di rimuoverne parte dal thread principale.

Usare i web worker per ridurre la complessità

I web worker, una funzionalità web spesso sottoutilizzata ma ampiamente disponibile, ti consentono di spostare il lavoro fuori dal thread principale.

I web worker consentono a una PWA di eseguire JavaScript al di fuori del thread principale.

Esistono tre tipi di worker.

Il lavoratore dedicato, che viene comunemente utilizzato per descrivere i web worker, può essere utilizzato da un singolo script in una singola istanza in esecuzione di una PWA. Quando possibile, il lavoro che non interagisce direttamente con il DOM deve essere spostato a un web worker per migliorare le prestazioni.

I Worker condivisi sono simili ai worker dedicati, ad eccezione del fatto che più script possono condividerli tra più finestre aperte. Ciò fornisce i vantaggi di un worker dedicato, ma con uno stato e un contesto interno condivisi tra finestre e script.

Un worker condiviso potrebbe, ad esempio, gestire l'accesso e le transazioni per l'IndexedDB di una PWA e trasmettere i risultati delle transazioni su tutti gli script di chiamata per consentire loro di reagire ai cambiamenti.

L'ultimo web worker è trattato in modo esaustivo in questo corso: i service worker, che fungono da proxy per le richieste di rete e vengono condivisi tra tutte le istanze di una PWA.

Fai una prova

È tempo di programmazione! Crea una PWA da zero in base a tutto ciò che hai imparato in questo modulo.

Risorse