Supercharged livestreamblog - Codesplitsing

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

Diverse tips & trucs in deze aflevering

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

Kortom

Hoe je code kunt splitsen via route-gebaseerde chunking:

  1. Vraag een lijst op met uw toegangspunten.
  2. Haal de module-afhankelijkheden van al deze toegangspunten eruit.
  3. Vind gedeelde afhankelijkheden tussen alle toegangspunten.
  4. Bundel de gedeelde afhankelijkheden.
  5. Herschrijf de toegangspunten.

Code splitsen versus route-gebaseerde chunking

Code splitsen en route-gebaseerde chunking zijn nauw verwant en worden vaak door elkaar gebruikt. Dit heeft tot verwarring geleid. Laten we proberen dit op te helderen:

  • Code splitsen : Code splitsen is het proces waarbij je je code opsplitst in meerdere bundels. Als je niet één grote bundel met al je JavaScript naar de klant verzendt, gebruik je code splitsen. Een specifieke manier om je code te splitsen is door middel van route-gebaseerde chunking.
  • Routegebaseerde chunking : Routegebaseerde chunking creëert bundels die gerelateerd zijn aan de routes van je app. Door je routes en hun afhankelijkheden te analyseren, kunnen we bepalen welke modules in welke bundel worden geplaatst.

Waarom code splitsen?

Losse modules

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

Bundelen

Bundelen, waarbij al je modules in één bundel worden samengevoegd, zorgt ervoor dat de browser na één retourronde alle benodigde code heeft en de code sneller kan uitvoeren. Dit dwingt de gebruiker echter om veel onnodige code te downloaden, waardoor bandbreedte en tijd verloren gaan. Bovendien resulteert elke wijziging in een van onze originele modules in een wijziging in de bundel, waardoor een gecachte versie van de bundel ongeldig wordt. Gebruikers moeten de hele bundel opnieuw downloaden.

Code splitsen

Code splitsen is de gulden middenweg. We zijn bereid extra roundtrips te investeren om de netwerkefficiëntie te verhogen door alleen te downloaden wat we nodig hebben, en om de cache-efficiëntie te verbeteren door het aantal modules per bundel aanzienlijk te verkleinen. Als de bundeling goed is uitgevoerd, zal het totale aantal roundtrips veel lager zijn dan bij losse modules. Tot slot zouden we gebruik kunnen maken van preloadingmechanismen zoals link[rel=preload] om indien nodig extra roundtrio-tijden te besparen.

Stap 1: Maak een lijst van uw toegangspunten

Dit is slechts één 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 hiervoor een speciaal JSON-bestand gebruikt met een lijst van alle toegangspunten.

Babel gebruiken om JavaScript te verwerken

Babel wordt vaak gebruikt voor "transpiling": het verwerken van de allernieuwste JavaScript-code en het omzetten ervan naar een oudere versie van JavaScript, zodat meer browsers de code kunnen uitvoeren. De eerste stap hierbij is het parsen van de nieuwe JavaScript-code met een parser (Babel gebruikt Babylon ) die de code omzet in een zogenaamde "Abstract Syntax Tree" (AST). Zodra de AST is gegenereerd, analyseert en vervormt een reeks plugins de AST.

We gaan Babel intensief gebruiken om de import van een JavaScript-module te detecteren (en later te manipuleren). Je zou in de verleiding kunnen komen om reguliere expressies te gebruiken, maar reguliere expressies zijn niet krachtig genoeg om een ​​taal correct te parsen en zijn moeilijk te onderhouden. Vertrouwen op beproefde tools zoals Babel bespaart je veel hoofdpijn.

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 plugin kan een visitor leveren. Het bezoekersobject bevat een functie voor elk knooppunttype dat de plugin wil verwerken. 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 het bovenstaande voorbeeld wordt de ImportDeclaration() -methode aangeroepen voor elke import in het bestand. Om meer inzicht te krijgen in knooppunttypen en de AST, kunt u een kijkje nemen op astexplorer.net .

Stap 2: De module-afhankelijkheden extraheren

Om de afhankelijkheidsboom van een module te bouwen, parseren we die module en maken we een lijst van alle modules die deze importeert. We moeten ook die afhankelijkheden parseren, aangezien die op hun beurt mogelijk ook afhankelijkheden hebben. Een klassiek voorbeeld 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 set afhankelijkheidsbomen hebben – een afhankelijkheidsbos, als je het zo wilt noemen – kunnen we de gedeelde afhankelijkheden vinden door te zoeken naar knooppunten die in elke boom voorkomen. We zullen ons bos platmaken en dedupliceren 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: Gedeelde afhankelijkheden bundelen

Om onze set gedeelde afhankelijkheden te bundelen, kunnen we alle modulebestanden samenvoegen. Bij deze aanpak ontstaan ​​er twee problemen: het eerste probleem is dat de bundel nog steeds import -statements bevat die de browser ertoe aanzetten om resources op te halen. Het tweede probleem is dat de afhankelijkheden van de afhankelijkheden niet gebundeld zijn. Omdat we dit al eerder hebben gedaan, gaan we een andere Babel-plugin schrijven.

De code is vrijwel gelijk aan die van onze eerste plugin, maar in plaats van alleen de imports te extraheren, verwijderen we ze ook en voegen we een gebundelde versie van het geïmporteerde bestand toe:

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 de invoerpunten

Voor de laatste stap schrijven we nog een Babel-plugin. Deze verwijdert alle imports van modules die in de gedeelde bundel zitten.

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 behoorlijke rit, nietwaar? Vergeet niet dat ons doel voor deze aflevering was om code splitsen uit te leggen en te demystificeren . Het resultaat werkt – maar het is specifiek voor onze demosite en zal in het generieke geval hopeloos falen. Voor productie raad ik aan om te vertrouwen op gevestigde tools zoals WebPack, RollUp, enzovoort.

Je vindt onze code in de GitHub-repository .

Tot de volgende keer!