Rychlejší statické sestavení webu Část 1 – Zpracujte pouze to, co potřebujete

Statické stránky získávají na popularitě. Velcí vydavatelé, jako je Smashing Magazine, spoléhají na generování statického webu, aby zobrazoval obsah rychleji. A dělají to bez obav o opravy zabezpečení nebo škálovatelné nastavení serveru. Vše, co potřebujete pro generování statického webu, je služba CI, která spustí vaši sestavu, a poskytovatel statického hostingu, který bude obsluhovat vaše vygenerované statické soubory, které pak obohatíme o technologie bez serveru.

Jsem velkým zastáncem přístupu ke statickým webům, ale tento přístup přichází s řadou výzev v závislosti na tom, čeho chcete dosáhnout. Jedním z problémů je zaručit krátké doby sestavení:generování souborů nějakou dobu trvá, a pokud chcete vygenerovat dvacet tisíc stránek, doba sestavení se prodlouží – což vede k frustraci a zpožděním v pracovním postupu publikování.

Možná si řeknete, že na tyto výzvy nenarazíte se svým projektem nebo osobním webem, a já jsem tomu před pár měsíci věřil. Ale nedávno jsem se potýkal s problémem, že sestavení trvá déle a déle. Můj soukromý web používá Contentful (založený na Vue.js). Je nasazený přes Netlify do Netlify a já jsem narazil na dobu sestavení hodně přes 10 minut – nepřijatelné.

V tomto prvním ze dvou článků o statických webech se s vámi podělím o to, jak můžete urychlit proces sestavování na Netlify pomocí vlastní mezipaměti. Druhý článek se bude věnovat implementaci přírůstkových sestavení pomocí Nuxt.js.

Krásný zástupný symbol obrázku s SQIP

Proč se doba výstavby tak prodloužila? Před několika měsíci jsem narazil na SQIP. SQIP je nový nástroj od Tobiase Baldaufa pro generování krásných zástupných obrázků SVG pomocí Primitive. Tyto zástupné symboly mohou zlepšit vnímaný výkon líně načtených obrázků. Primitive zkoumá obrázky a generuje SVG, které představují obrázek s primitivními tvary, které vypadají překvapivě dobře, když použijete efekt rozostření.

Pomocí těchto krásných náhledových obrázků uživatel ví, co může očekávat, když se začne načítat obrázek, což vede k lepšímu uživatelskému zážitku než u spinnerů nebo náhodného načítání grafiky.

Funguje to tak, že pod obrázek umístíte malou grafiku SVG, která se nakonec objeví a vybledne.

Pokud nemáte zájem o implementaci těchto dobře vypadajících zástupných obrázků a chcete si pouze přečíst o ukládání do mezipaměti na Netlify, můžete přejít přímo do sekce „Ukládání do mezipaměti pro vítězství“.

Generování náhledových obrázků pomocí SQIP

Funguje to takto – moje obrázky jsou uloženy v Contentful a pro generování náhledů SVG projdu těmito kroky:

  • Získání informací o všech aktivech uložených v Contentful
  • Stáhněte si všechny obrázky
  • Vygenerujte zástupné SVG obrázků
  • Vytvořte soubor JavaScript, který bude obsahovat všechny soubory SVG, abyste je mohli vložit později

Všechny následující části kódu jsou malými částmi celého delšího skriptu, který bude propojen na konci článku, a kód intenzivně využívá asynchronní funkce, díky nimž je zpracování asynchronních operací mnohem lepší! Výsledkem je, že kdykoli uvidíte await někde je umístěn uvnitř asynchronní funkce v celkové implementaci.

Podle osvědčených postupů vyžaduje výsledný skript všechny závislosti v horní části souboru, zatímco do částí kódu je vkládám těsně předtím, než je použiji, aby byly úryvky srozumitelnější.

Načíst všechna díla z Contentful

Získání všech informací o aktivech z Contentful API je jednoduché. Musím pouze inicializovat klienta Contentful SDK a getAssets funkce mi poskytuje informace, které potřebuji.

const contentful = require('contentful')
const client = contentful.createClient({ … })

//Getting asset information

// Contentful collection responses have a default limit 
// of 100 -> increase it to 1000 to avoid the need for
// pagination at this stage
const {items} = await client.getAssets({limit: 1000})
let images = items
  // only treat image files
  // there can also be pdfs and so one
  .filter(
    ({fields}) => fields.file && ['image/png', 'image/jpeg'].indexOf(fields.file.contentType) !== -1
  )
  // strip out useless information
  // and flatten data structure with needed information
  .map(({sys, fields}) => ({
    id: sys.id,
    revision: sys.revision,
    url: fields.file.url,
    filename: `${sys.id}-${sys.revision}.${fields.file.contentType.split('/')[1]}`
  }))

Nejprve musím odfiltrovat všechna aktiva, abych odstranil soubory, které nejsou PNG nebo JPEG. Poté se pomocí map zbavím všech metainformací, které mě nezajímají funkce.

V tuto chvíli mám pole images podržením id , revision a konkrétní obrázek url . Kolekce také obsahuje filename vlastnost, která je kombinací ID aktiva a jeho revize.

Propojení těchto dvou atributů je nezbytné, protože kdykoli aktualizuji dílo, chci také vygenerovat nový náhled SVG – zde vstupuje do hry číslo revize, protože se v tomto případě mění.

Stáhněte si obrázky a vytvořte SVG

S tímto shromažďováním informací o všech aktivech pro můj web pokračuji ve stahování všech aktiv. Balíček ke stažení, který jsem našel na npm, se perfektně hodí.

const download = require('download')
const IMAGE_FOLDER = '...'

// Downloading images for missing SVGs
await Promise.all(
  // map all image objects to Promises representing
  // the image download
  images.map(({url, filename}) => {
    return download(
      url.replace(/\/\//, 'https://'),
      IMAGE_FOLDER,
      { filename }
    )
  })
)

Všechny položky aktiv jsou namapovány na přísliby vrácené funkcí stahování a vše zabaleno do Promise.all abych si mohl být jistý, že všechny obrázky jsou staženy do předdefinovaného IMAGE_FOLDER . Tady svítí async/await!

SQIP it

SQIP lze používat programově, což znamená, že můžete modul vyžadovat a můžete začít.

const {writeFile} = require('fs-extra')
const sqip = require('sqip')

// Writing of generated preview SVGs to disk
await Promise.all(images.map(({id, revision, filename}) => {
  const {final_svg} = sqip({
    filename: path.join(IMAGE_FOLDER, filename),
    numberOfPrimitives: 10,
    mode: 0,
    blur: 0
  })

  return writeFile(
    path.join(IMAGE_FOLDER, `${id}-${revision}.svg`),
    final_svg
  )
}))

sqip modul však nezapisuje soubory na disk. Vrací objekt včetně vygenerovaného SVG v final_svg vlastnictví. Můžete říci, že bych mohl použít hodnotu řetězce SVG a uložit SVG přímo do images sbírku, ale nejprve jsem zapsal SVG na disk.

Používám také balíček fs-extra, který poskytuje některé pohodlné metody oproti nativnímu fs modul a také mapuje funkce zpětného volání na jejich slíbené verze, abych nemusel dělat, např. writeFile na základě mých slibů.

To má tu výhodu, že se mohu rychle podívat na vygenerované soubory SVG na mém pevném disku a také se mi to bude hodit později v sekci ukládání do mezipaměti tohoto článku.

Modul SQIP přijímá následující argumenty:

  • numberOfPrimitives definuje počet tvarů (10 tvarů mi funguje s poměrně malými soubory SVG, ale dobrý náhled)
  • mode definuje, jaký typ tvarů má vygenerované SVG obsahovat (trojúhelník, čtverec, kruhy, všechny tyto)
  • blur definuje úroveň aplikovaného rozostření (v SVG jsem šel bez rozmazání, protože jsem zjistil, že výsledek rozostření CSS vede k lepším výsledkům)

Přečtěte si SVG

Dalším krokem bylo přečíst všechny vygenerované soubory SVG a připravit je k použití v mé JavaScriptové aplikaci.

const {readFile} = require('fs-extra')

// Reading SVGs
images = await Promise.all(images.map(async (image) => {
  const svg = await readFile(path.join(IMAGE_FOLDER, `${image.id}-${image.revision}.svg`), 'utf8')


  // add ID to SVG for easier debugging later
  image.svg = svg.replace('<svg', `<svg id="${image.id}"`)

  return image
}))

fs-extra také poskytuje readFile funkce, takže jsem připraven plnit sliby.

Kolekce objektů aktiv se obohatí o hodnotu řetězce vygenerovaného SVG. Tato řetězcová hodnota také přidá ID díla do SVG, takže později uvidím, jaké dílo bylo základem pro konkrétní náhledový obrázek SVG.

Namapujte SVG na JavaScript, abyste je měli k dispozici v Nuxtu .js (nebo jakékoli jiné prostředí JS)

Poslední krok – shromažďování aktiv nyní zahrnuje meta informace a také vygenerované stringified SVG v svg vlastnost každé položky. Je čas jej znovu použít v prostředí JavaScriptu.

const JS_DESTINATION = path.resolve(__dirname, 'image-map.js')

// Writing JS mapping file
writeFile(
  JS_DESTINATION,
  `export default {\n  ${images.map(({id, svg}) => `'${id}': '${svg}'`).join(', ')}\n}\n`
)

Tento krok zapíše soubor JavaScript, který je v mém úložišti git ignorován. Soubor JavaScript exportuje objekt, který definuje každý SVG prostřednictvím ID aktiva. Tímto způsobem bych mohl později importovat tento soubor a použít ID aktiva k získání vygenerovaného SVG při spuštění a sestavení.

import imageMap from '~/plugins/image-map.js'

const preview = imageMap[this.asset.sys.id] || null

Spuštění výsledného skriptu, včetně pěkných protokolovacích zpráv, trvá dvě až čtyři minuty na mém MacBooku Pro pro 55 aktiv (v závislosti na tom, co ještě běží na mém počítači).

▶ ./scripts/sqip-it-without-cache               [19:46:49]
Getting asset information
Asset information queried - 55 assets
// --------------------------------------------
Downloading images for SVGs...
Images downloaded
// --------------------------------------------
Creating SVGs...
SVGs created
// --------------------------------------------
Reading SVGs...
SVGs read
// --------------------------------------------
Writing JS mapping file
JS file written
// --------------------------------------------
▶                                                [19:50:46]

Když však běží na Netlify, spuštění skriptu může snadno trvat pět až sedm minut, což má za následek dobu sestavení kolem zmíněných deseti minut.

Opakovaná regenerace není optimální přístup. S tímto skriptem by každá sestava dělala stejnou těžkou práci – znovu a znovu. Kdykoli opakujete operace, ať už jde o optimalizace obrázků nebo jiné rozsáhlé výpočty, které zaberou několik minut, je čas to zlepšit.

Krása nepřetržitého zásobování spočívá v tom, že věci mohou být spuštěny pravidelně a rychle – deset minut na opravu překlepu do výroby není prostředí, se kterým se chci pro svůj malý web vypořádat.

Jak tedy tento nepořádek vyřeším?

Náhledy obrázků bych si mohl vygenerovat sám a také je nahrát do Contentful, což má nevýhodu v tom, že dva na sobě závisejí aktiva, se kterými se musím vypořádat (obrázek a náhled) – není možnost.

Mohl bych odevzdat náhled do úložiště git, ale vždy se cítím špatně, když do git svěřuji velké prostředky. Velké binární soubory nejsou to, pro co je git stvořen, a drasticky zvětšuje velikost úložiště – také žádná možnost.

Ukládání do mezipaměti pro výhru

Netlify spouští každé nasazení v kontejneru dockeru bez možnosti znovu použít věci z předchozího nasazení (kromě závislostí – ale nechci zneužívat složku node_modules pro své vlastní věci). Mým počátečním řešením byl bucket S3 fungující jako mezipaměťová vrstva během mých sestav.

Vrstva mezipaměti by obsahovala stažené obrázky a vygenerované náhledy z předchozího sestavení a vzhledem k ID a konvenci pojmenovávání revizí by kontrola existence souboru stačila, aby se zjistilo, jaké nové prostředky je třeba vygenerovat. Tento přístup fungoval dobře, ale pak se se mnou Phil z Netlify podělil o tajemství (ale pozor – není zdokumentováno a použití je na vlastní riziko).

Ukázalo se, že existuje složka, která přetrvává napříč sestaveními – /opt/build/cache/ . Tuto složku můžete použít k ukládání souborů napříč sestaveními, což vede k několika dalším krokům v mém skriptu, ale výrazně zkracuje dobu generování SVG:

  • Získání informací o všech aktivech uložených v Contentful
  • Zkontrolujte, jaké soubory SVG již byly vygenerovány
  • Stáhněte si chybějící obrázky
  • Vygenerujte zástupné SVG chybějících obrázků
  • Vytvořte soubor JavaScript, který bude obsahovat všechny soubory SVG, abyste je mohli vložit později

Definujte mezipaměť lokálně a v Netlify

Složka obrázků, kterou jsem definoval ve skriptu, se nyní stane složkou mezipaměti (SQIP_CACHE ) v závislosti na prostředí.

const isProduction = process.env.NODE_ENV === 'production'
const SQIP_CACHE = isProduction
  ? path.join('/', 'opt', 'build', 'cache', 'sqip')
  : path.resolve(__dirname, '.sqip')

Tímto způsobem bych mohl spustit skript na svém vývojovém počítači a umístit všechny soubory do složky, kterou git také ignoruje, ale při spuštění na Netlify používá trvalou složku.

Kontrola existujících vygenerovaných souborů

Pamatujte na images kolekce, kterou jsem dříve používal?

const {readFile} = require('fs-extra')

// Reading cached SVGs
images = await Promise.all(images.map(async (image) => {
  try {
    const svg = await readFile(`${SQIP_CACHE}/${image.id}-${image.revision}.svg`, 'utf8')
    if (svg.startsWith('<svg')) {
      image.svg = svg
    }
  } catch (e) {}

  return image
}))

Poté přidám další krok k předchozímu skriptu a zjistím, zda je ve složce mezipaměti k dispozici SVG se správným ID aktiva a kombinací revize.

Pokud ano, přečtěte si soubor a definujte svg vlastnost položky obrázku, pokud ne, pokračujte.

Generování nových náhledových souborů SVG

Generování souborů SVG zůstává stejné, až na to, že nyní mohu zkontrolovat, zda je již k dispozici vygenerovaná hodnota SVG:

// Creating missing SVGs...
await Promise.all(images.map(({id, revision, filename, svg}) => {
  // if there was an SVG in the cache
  // do nothing \o/
  if (!svg) {
    const command = `${SQIP_EXEC} -o ${id}-${revision}.svg -n 10 -m 0 -b 0 ${filename}`

    return execute(
      command,
      {cwd: SQIP_CACHE}
    )
  }

  return Promise.resolve()
}))

S vylepšeným skriptem se mohu vyhnout opakovaným výpočtům a časy sestavení na mém místním počítači a Netlify klesly ani na jednu sekundu pro opakované sestavení s naplněnou mezipamětí!

Pokud si s tím chcete pohrát, poskytnutá podstata obsahuje vše, co potřebujete ke generování a ukládání krásných náhledů obrázků do mezipaměti s prostorem pro příklady obsahu.

Přemýšlejte o přepínači zabíjení – vymazání mezipaměti

Byla tu ještě jedna poslední věc – ukládání do mezipaměti může být obtížné a zvláště když implementujete mezipaměť na vzdálených serverech, ke kterým nemáte přístup, měli byste být schopni vše zahodit a začít znovu.

V mém případě běžícím na Netlify jsem zvolil vlastní webhook, který vymaže adresář mezipaměti, než se cokoliv stane, když tento webhook spustí sestavení.

const {emptyDir} = require('fs-extra')

if (process.env.WEBHOOK_TITLE === 'CLEAR_CUSTOM_CACHE') {
  console.log(`Clearing ${SQIP_CACHE}`)
  await emptyDir(SQIP_CACHE)
}

Problém vyřešen!

Udržujte své sestavy co nejrychleji

Přidání mezipaměti náhledu výrazně zlepšilo zážitek z vytváření mého statického webu. Líbí se mi nastavení Contentful, Nuxt.js a Netlify a teď, když jsou časy sestavení opět na třech minutách, mohu začít přemýšlet o dalším vylepšení – urychlení generování statických HTML souborů.

Můj plán je používat složky mezipaměti Netlify pouze ke generování konkrétních souborů, nikoli celého webu. Když například přidám nový příspěvek na blog, stačí aktualizovat jen několik stránek, ne všechny ze 150 stránek a všechny soubory JavaScript, obrázky a CSS. To je výpočet, kterému se lze nyní vyhnout.

Koncový bod synchronizace Contentful poskytuje podrobné informace o tom, co se změnilo ve srovnání s poslední synchronizací, a dokonale se hodí pro tento případ použití, který umožňuje inkrementální sestavení – téma, se kterým se potýká mnoho velkých generátorů statických webů. Brzy si o tom můžete přečíst. Dám vám vědět!