Supercharged livestreamblog - Codesplitsing

In onze meest recente Supercharged Livestream hebben we codesplitsing en routegebaseerde chunking geïmplementeerd. Met HTTP/2 en native ES6-modules zullen deze technieken essentieel worden voor het efficiënt laden en cachen van scriptbronnen.

Diverse tips & tricks in deze aflevering

  • asyncFunction().catch() met error.stack : 9:55
  • Modules en nomodule attribuut op <script> tags: 7:30
  • promisify() in knooppunt 8: 17:20

TL; DR

Code splitsen via op routes gebaseerde chunking:

  1. Zorg voor een lijst met uw toegangspunten.
  2. Extraheer de module-afhankelijkheden van al deze toegangspunten.
  3. Vind gedeelde afhankelijkheden tussen alle toegangspunten.
  4. Bundel de gedeelde afhankelijkheden.
  5. Herschrijf de toegangspunten.

Het splitsen van code versus op routes gebaseerde chunking

Het splitsen van codes en op routes gebaseerde chunking zijn nauw verwant en worden vaak door elkaar gebruikt. Dit heeft voor enige verwarring gezorgd. Laten we proberen dit op te helderen:

  • Codesplitsing : Codesplitsing is het proces waarbij uw code in meerdere bundels wordt gesplitst. Als u niet één grote bundel met al uw JavaScript naar de klant verzendt, doet u aan codesplitsing. Een specifieke manier om uw code te splitsen is het gebruik van op routes gebaseerde chunking.
  • Op route gebaseerde chunking : Op route gebaseerde chunking maakt bundels die gerelateerd zijn aan de routes van uw app. Door uw routes en hun afhankelijkheden te analyseren, kunnen we veranderen welke modules in welke bundel gaan.

Waarom code splitsen?

Losse modules

Met native ES6-modules kan elke JavaScript-module zijn eigen afhankelijkheden importeren. Wanneer de browser een module ontvangt, zullen alle import extra ophaalacties activeren om de modules te bemachtigen die nodig zijn om de code uit te voeren. Al deze modules kunnen echter hun eigen afhankelijkheden hebben. Het gevaar is dat de browser eindigt met een reeks ophaalacties die meerdere keren heen en weer gaan voordat de code uiteindelijk kan worden uitgevoerd.

Bundelen

Bundelen, waarbij al uw modules in één enkele bundel worden samengevoegd, zorgt ervoor dat de browser na één retourvlucht over alle code beschikt die hij nodig heeft en de code sneller kan uitvoeren. Dit dwingt de gebruiker echter veel code te downloaden die niet nodig is, waardoor bandbreedte en tijd worden verspild. Bovendien zal elke wijziging aan een van onze originele modules resulteren in een wijziging in de bundel, waardoor elke in de cache opgeslagen versie van de bundel ongeldig wordt. Gebruikers zullen het hele ding opnieuw moeten downloaden.

Code-splitsing

Het splitsen van codes is de middenweg. We zijn bereid extra retourvluchten te investeren om netwerkefficiëntie te verkrijgen door alleen te downloaden wat we nodig hebben, en een betere caching-efficiëntie door het aantal modules per bundel veel kleiner te maken. Als de bundeling goed gebeurt, zal het totaal aantal rondreizen veel lager zijn dan bij losse modules. Ten slotte zouden we gebruik kunnen maken van voorlaadmechanismen zoals link[rel=preload] om indien nodig extra rondetrio-tijden te besparen.

Stap 1: Zorg voor een lijst met uw toegangspunten

Dit is slechts een van de vele benaderingen, maar in de aflevering hebben we de sitemap.xml van de website geparseerd om de toegangspunten tot onze website te verkrijgen. Meestal wordt een speciaal JSON-bestand gebruikt met alle toegangspunten.

Babel gebruiken om JavaScript te verwerken

Babel wordt vaak gebruikt voor ‘transpiling’: het consumeren van geavanceerde JavaScript-code en deze omzetten in een oudere versie van JavaScript, zodat meer browsers de code kunnen uitvoeren. De eerste stap hier is het parseren van het nieuwe JavaScript met een parser (Babel gebruikt babylon ) die de code omzet in een zogenaamde “Abstract Syntax Tree” (AST). Zodra de AST is gegenereerd, analyseert en verminkt een reeks plug-ins de AST.

We gaan intensief gebruik maken van babel om de import van een JavaScript-module te detecteren (en later te manipuleren). Je zou in de verleiding kunnen komen om je toevlucht te nemen tot reguliere expressies, maar reguliere expressies zijn niet krachtig genoeg om een ​​taal goed te ontleden en zijn moeilijk te onderhouden. Vertrouwen op beproefde tools zoals Babel bespaart u veel kopzorgen.

Hier is een eenvoudig voorbeeld van het uitvoeren van Babel met een aangepaste plug-in:

const plugin = {
  visitor: {
    ImportDeclaration(decl) {
      /* ... */
    }
  }
}
const {code} = babel.transform(inputCode, {plugins: [plugin]});

Een plug-in kan een visitor bieden. De bezoeker bevat een functie voor elk knooppunttype dat de plug-in wil afhandelen. Wanneer een knooppunt van dat type wordt aangetroffen tijdens het doorlopen van de AST, wordt de corresponderende functie in het visitor aangeroepen met dat knooppunt als parameter. In bovenstaand voorbeeld wordt voor iedere import in het bestand de methode ImportDeclaration() aangeroepen. Om meer gevoel te krijgen voor knooppunttypen en de AST, kijk eens op astexplorer.net .

Stap 2: Extraheer de module-afhankelijkheden

Om de afhankelijkheidsboom van een module op te bouwen, zullen we die module parseren en een lijst maken van alle modules die hij importeert. We moeten deze afhankelijkheden ook analyseren, omdat deze op hun beurt ook afhankelijkheden kunnen hebben. Een klassiek geval van recursie!

async function buildDependencyTree(file) {
  let code = await readFile(file);
  code = code.toString('utf-8');

  // `dep` will collect all dependencies of `file`
  let dep = [];
  const plugin = {
    visitor: {
      ImportDeclaration(decl) {
        const importedFile = decl.node.source.value;
        // Recursion: Push an array of the dependency’s dependencies onto the list
        dep.push((async function() {
          return await buildDependencyTree(`./app/${importedFile}`);
        })());
        // Push the dependency itself onto the list
        dep.push(importedFile);
      }
    }
  }
  // Run the plugin
  babel.transform(code, {plugins: [plugin]});
  // Wait for all promises to resolve and then flatten the array
  return flatten(await Promise.all(dep));
}

Stap 3: Vind gedeelde afhankelijkheden tussen alle toegangspunten

Omdat we een reeks afhankelijkheidsbomen hebben – een afhankelijkheidsbos als je wilt – kunnen we de gedeelde afhankelijkheden vinden door te zoeken naar knooppunten die in elke boom voorkomen. We zullen ons bos afvlakken en ontdubbelen en filteren om alleen de elementen te behouden die in alle bomen voorkomen.

function findCommonDeps(depTrees) {
  const depSet = new Set();
  // Flatten
  depTrees.forEach(depTree => {
    depTree.forEach(dep => depSet.add(dep));
  });
  // Filter
  return Array.from(depSet)
    .filter(dep => depTrees.every(depTree => depTree.includes(dep)));
}

Stap 4: Bundel gedeelde afhankelijkheden

Om onze set gedeelde afhankelijkheden te bundelen, kunnen we alle modulebestanden gewoon samenvoegen. Er doen zich twee problemen voor bij het gebruik van deze aanpak: Het eerste probleem is dat de bundel nog steeds import zal bevatten waardoor de browser zal proberen bronnen op te halen. Het tweede probleem is dat de afhankelijkheden van de afhankelijkheden niet zijn gebundeld. Omdat we het al eerder hebben gedaan, gaan we nog een babel-plug-in schrijven.

De code lijkt redelijk op onze eerste plug-in, maar in plaats van alleen de importbestanden te extraheren, zullen we ze ook verwijderen en een gebundelde versie van het geïmporteerde bestand invoegen:

async function bundle(oldCode) {
  // `newCode` will be filled with code fragments that eventually form the bundle.
  let newCode = [];
  const plugin = {
    visitor: {
      ImportDeclaration(decl) {
        const importedFile = decl.node.source.value;
        newCode.push((async function() {
          // Bundle the imported file and add it to the output.
          return await bundle(await readFile(`./app/${importedFile}`));
        })());
        // Remove the import declaration from the AST.
        decl.remove();
      }
    }
  };
  // Save the stringified, transformed AST. This code is the same as `oldCode`
  // but without any import statements.
  const {code} = babel.transform(oldCode, {plugins: [plugin]});
  newCode.push(code);
  // `newCode` contains all the bundled dependencies as well as the
  // import-less version of the code itself. Concatenate to generate the code
  // for the bundle.
  return flatten(await Promise.all(newCode)).join('\n');
}

Stap 5: Herschrijf toegangspunten

Voor de laatste stap zullen we nog een Babel-plug-in schrijven. Het is zijn taak om alle import van modules die zich in de gedeelde bundel bevinden, te verwijderen.

async function rewrite(section, sharedBundle) {
  let oldCode = await readFile(`./app/static/${section}.js`);
  oldCode = oldCode.toString('utf-8');
  const plugin = {
    visitor: {
      ImportDeclaration(decl) {
        const importedFile = decl.node.source.value;
        // If this import statement imports a file that is in the shared bundle, remove it.
        if(sharedBundle.includes(importedFile))
          decl.remove();
      }
    }
  };
  let {code} = babel.transform(oldCode, {plugins: [plugin]});
  // Prepend an import statement for the shared bundle.
  code = `import '/static/_shared.js';\n${code}`;
  await writeFile(`./app/static/_${section}.js`, code);
}

Einde

Dit was een hele rit, nietwaar? Houd er rekening mee dat ons doel voor deze aflevering was om het splitsen van codes uit te leggen en te demystificeren . Het resultaat werkt, maar het is specifiek voor onze demosite en zal in het algemene geval vreselijk mislukken. Voor productie raad ik aan te vertrouwen op gevestigde tools zoals WebPack, RollUp, enz.

Je kunt onze code vinden in de GitHub-repository .

Tot de volgende keer!