Pikselit Etenemään: AI-Pohjainen Kuvantamisohjelmisto Biotechin Vuoden 2030 Vallankumouksessa
TypeScript-projektin hidastuminen tapahtuu usein niin vähitellen, ettei sitä huomaa heti.
Alussa npm run build valmistuu nopeasti. Kehittäjä tekee muutoksen, ajaa buildin ja jatkaa töitä. Sitten projektiin tulee lisää ominaisuuksia, muutama uusi kirjasto ja ehkä toinen sovellus samaan repositoryyn.
Jossain vaiheessa build ei enää tunnukaan nopealta.
Kehittäjä tekee pienen muutoksen ja odottaa. Sitten toisen ja odottaa uudelleen.
Kun tähän menee joka päivä useita kertoja, muutama ylimääräinen minuutti alkaa tuntua yllättävän paljon.
Tässä vaiheessa on helppo syyttää TypeScriptiä. Se ei kuitenkaan aina ole oikea kohde.
Ensimmäinen asia, jonka tarkistaisin
➠ Jos joku sanoo, että “TypeScript build on hidas”, haluaisin ensin tietää, mikä vaihe oikeasti vie ajan.
➠ npm run build ei välttämättä tarkoita pelkkää TypeScriptin kääntämistä. Samassa komennossa voi olla mukana bundlaus, linttaus, testejä, koodin generointia tai muita vaiheita.
➠ Esimerkiksi jos koko build kestää 90 sekuntia, mutta tsc käyttää siitä vain 15 sekuntia, TypeScriptin optimointi ei ratkaise ongelmaa.
➠ Tämä kuulostaa itsestään selvältä, mutta tuotantoprojektissa aikaa voi mennä yllättävän helposti väärän asian tutkimiseen.
➠ Joskus ensimmäinen hyödyllinen testi onkin hyvin yksinkertainen:
tsc –extendedDiagnostics
➠ Sen jälkeen alkaa selvitä, mitä compiler oikeasti tekee.
Projektin kasvaessa myös tarkistettavaa tulee lisää
Pieni TypeScript-projekti antaa helposti väärän turvallisuuden tunteen.
Kun lähdekoodia on muutama sata tiedostoa, kaikki tuntuu nopealta. Myöhemmin tiedostoja voi olla tuhansia, riippuvuuksia paljon enemmän ja tyyppien väliset suhteet huomattavasti monimutkaisempia.
Compiler ei katso vain muuttunutta tiedostoa irrallisena palasena.
Se joutuu ymmärtämään myös sitä ympäröivää tyyppijärjestelmää ja riippuvuuksia.
Tämän vuoksi yksi uusi ominaisuus voi joskus vaikuttaa build-aikaan paljon enemmän kuin sen sisältämä koodimäärä antaa olettaa.
Varsinkin isoissa frontend-projekteissa tämä tulee vastaan melko luonnollisesti.
Kaikki TypeScript-koodi ei ole compilerille samanlaista
Kymmenen tuhatta yksinkertaista tyyppiä ei välttämättä ole ongelma.
Sen sijaan pieni määrä erittäin monimutkaisia tyyppejä voi olla.
Esimerkiksi geneeriset tyypit, conditional typet ja useiden kirjastojen päällekkäiset tyyppimäärittelyt voivat saada type-checkingin tekemään paljon enemmän työtä.
Koodista voi siis tulla hitaampaa ilman, että koodirivien määrä kasvaa erityisen paljon.
Tämä on yksi syy siihen, miksi pelkkä projektin koko ei vielä kerro, miksi build hidastuu.
Joskus ongelma on enemmän siinä, mitä TypeScript joutuu päättelemään, kuin siinä, kuinka paljon lähdekoodia repositoryssä on.
Yksi kirjasto voi muuttaa tilannetta
➠ Dependencyjen lisääminen on tavallista. Useimmiten uusi kirjasto ei aiheuta mitään näkyvää ongelmaa.
➠ Joskus vaikutus on kuitenkin yllättävä.
➠ Kirjaston runtime voi olla pieni, mutta sen mukana tulevat tyyppimääritykset voivat olla melko raskaita. Jos tyypit liittyvät vielä oman sovelluksen monimutkaisiin geneerisiin rakenteisiin, compilerin työmäärä kasvaa.
➠ Siksi hidastumisen ajankohta kannattaa ottaa vakavasti.
➠ Jos build oli maanantaina nopea ja uuden dependency-päivityksen jälkeen selvästi hitaampi, en lähtisi ensimmäisenä muuttamaan koko tsconfig.json-tiedostoa.
➠ Katsoisin ensin sitä dependencyä.
➠ Se voi olla paljon nopeampi tapa löytää syy.
include voi olla liian väljä
Jos TypeScript-projektiin otetaan mukaan käytännössä koko repository, compilerille voi päätyä paljon tavaraa, jota sen ei tarvitse käsitellä.
Esimerkiksi repositoryssä voi olla:
src/
tests/
scripts/
generated/
tools/
dist/
Jos kaikki nämä päätyvät saman projektin piiriin, työmäärä voi kasvaa aivan turhaan.
Hyvä lähtökohta on miettiä, mitä kyseinen TypeScript-projekti oikeasti tarvitsee.
Esimerkiksi:
{
“include”: [“src”]
}
voi olla järkevämpi kuin koko repositoryn ottaminen mukaan.
Tämä riippuu tietenkin projektista. Testit tai generated-koodi voivat aivan hyvin kuulua samaan kokonaisuuteen.
Pointti ei ole tehdä include-asetuksesta mahdollisimman pientä.
Pointti ei ole tehdä include-asetuksesta mahdollisimman pientä.
Generated code on yksi asia, joka unohtuu helposti
Monissa projekteissa TypeScript-koodia ei kirjoiteta kokonaan käsin.
GraphQL, OpenAPI, ORM:t ja erilaiset sisäiset työkalut voivat tuottaa sitä automaattisesti.
Alussa generated-koodia on ehkä vähän.
Vuoden kuluttua sitä voi olla paljon.
Jos generated-hakemistossa on valtava määrä tiedostoja ja ne kuuluvat samaan TypeScript-projektiin, compiler joutuu käsittelemään ne muiden lähdekoodien mukana.
Tässä kannattaa katsoa, onko kaikki todella tarpeellista.
Jos jokin generoitu osa on itsenäinen kokonaisuus, sen erottaminen omaksi projektikseen voi olla järkevämpää kuin sen vetäminen aina koko sovelluksen mukana.
Monorepossa ongelma näkyy vielä selvemmin
➠ Monorepo tekee tästä kiinnostavan.
➠ Repositoryssä voi olla esimerkiksi web-sovellus, admin-käyttöliittymä, yhteinen UI-kirjasto ja joukko backend-packageja.
➠ Alussa rakenne on siisti.
➠ Myöhemmin jokainen package alkaa riippua vähän toisesta.
➠ Sitten yhteinen package riippuu jostain, joka tuo mukanaan taas muita tyyppejä.
➠ Kun yksi pieni asia muuttuu, vaikutusalue voi olla yllättävän suuri.
➠ Tässä kohtaa project references voi auttaa. Se ei kuitenkaan ole ratkaisu vain siksi, että repository on monorepo.
➠ Jos packagejen rajat ovat huonot, references-rakenteella voidaan korkeintaan tehdä sekavasta järjestelmästä vähän hienommin organisoitu sekava järjestelmä.
➠ Ensin kannattaa ymmärtää riippuvuudet.
skipLibCheck voi antaa nopeutta, mutta en käyttäisi sitä sokkona
skipLibCheck tulee vastaan lähes jokaisessa keskustelussa TypeScript-buildin nopeuttamisesta.
{
“compilerOptions”: {
“skipLibCheck”: true
}
}
Sillä voidaan vähentää riippuvuuksien declaration-tiedostojen tarkistamiseen käytettävää aikaa.
Se voi olla aivan perusteltu ratkaisu.
Mutta ennen kuin asetus lisätään vain suorituskyvyn vuoksi, kannattaa ymmärtää mitä sillä menetetään.
Jos projektissa on esimerkiksi riippuvuuksien tyyppiongelmia, niitä ei välttämättä enää tarkisteta samalla tavalla.
Suurelle tuotantosovellukselle tämä voi olla hyväksyttävä kompromissi.
Pienessä projektissa en välttämättä edes koskisi siihen.
Kaikkia optimointeja ei tarvitse ottaa käyttöön vain siksi, että ne ovat olemassa.
Incremental build on eri asia kuin nopea clean build
Kehittäjän arjessa yksi tärkeimmistä asioista on se, kuinka nopeasti pieni muutos voidaan tarkistaa uudelleen.
Siinä incremental compilation voi auttaa.
TypeScript voi säilyttää tietoa aikaisemmasta buildistä ja käyttää sitä seuraavalla kerralla.
Mutta CI:ssä tilanne voi olla erilainen.
Jos jokainen build tehdään puhtaassa ympäristössä ilman hyödyllistä cachea, paikallisesta incremental-buildistä ei ole siellä samanlaista hyötyä.
Siksi kannattaa erottaa kaksi asiaa:
Kuinka nopeasti kehittäjä saa muutoksen tarkistettua?
ja
Kuinka nopeasti täysin puhdas tuotantobuild valmistuu?
Ne eivät ole sama ongelma.
Joskus TypeScript näyttää syylliseltä, vaikka bundler on hidas
➠ Tämä on tullut vastaan monissa moderneissa JavaScript-projekteissa siinä muodossa, että kaikki kutsutaan vain “buildiksi”.
➠ Todellisuudessa taustalla voi olla useita työkaluja.
➠ Vite, Webpack, esbuild, SWC ja muut työkalut voivat käsitellä TypeScriptiä eri tavoin. TypeScript compileria voidaan käyttää type-checkingiin, vaikka varsinainen JavaScriptin generointi tehdään toisella työkalulla.
➠ Jos siis build hidastuu, kannattaa selvittää myös se, mikä työkalu käyttää ajan.
➠ Muuten voi käydä niin, että TypeScript-konfiguraatiota säädetään tuntikausia, vaikka todellinen pullonkaula on bundlerissa.
TypeScriptin muistinkäyttö kertoo joskus enemmän
Jos projekti on suuri, buildin hidastumiseen voi liittyä myös kasvava muistinkäyttö.
Tämä näkyy joskus kehittäjän koneella oudosti.
Build alkaa nopeasti, kone tekee hetken töitä ja sitten suoritus hidastuu selvästi. CPU ei välttämättä edes näytä koko ajan täydeltä.
Silloin kannattaa tarkistaa, joutuuko Node prosessin muistirajoille tai syntyykö paljon garbage collection -työtä.
Muistin lisääminen voi auttaa, jos ongelma todella on siinä.
Se ei kuitenkaan korjaa projektin rakennetta.
Jos compiler joutuu pitämään valtavan määrän tietoa muistissa vain siksi, että kaikki repositoryn osat kuuluvat yhteen projektiin, suurempi heap on lähinnä tapa ostaa aikaa.
Buildin hidastumista kannattaa seurata ennen kuin se muuttuu ongelmaksi
Tämä on ehkä käytännöllisin asia koko aiheessa.
Jos build-aikaa ei mitata, ongelma huomataan vasta kun kehittäjät alkavat valittaa siitä.
Silloin ei enää tiedetä, milloin hidastuminen alkoi.
Parempi tilanne olisi esimerkiksi se, että CI:ssä näkyisi build-aika version tai commitin mukaan.
Silloin voidaan huomata, että build oli pitkään noin 25 sekuntia ja nousi yhtäkkiä 45 sekuntiin tietyn muutoksen jälkeen.Tämä on yksi syy siihen, miksi pelkkä projektin koko ei vielä kerro, miksi build hidastuu.
Siitä on jo paljon helpompi lähteä etsimään syytä.
Ilman tätä tietoa keskustelu muuttuu helposti tällaiseksi:
“TypeScript on nykyään jotenkin todella hidas.”
Se ei vielä kerro, mitä pitäisi korjata.
Mitä kannattaa tehdä, jos build on jo hidas?
➠ En lähtisi tekemään isoa refaktorointia ensimmäisenä.
➠ Ottaisin ensin yhden nykyisen buildin ja mittaisin sen.
➠ Sen jälkeen katsoisin TypeScriptin omista diagnostiikkatiedoista, kuinka paljon tiedostoja käsitellään ja kuinka paljon aikaa type-checkingiin kuluu.
➠ Jos tiedostomäärä näyttää liian suurelta, tarkistaisin projektin rajauksen.
➠ Jos type-checking on raskas, katsoisin riippuvuuksia ja erityisesti monimutkaisia tyyppirakenteita.
➠ Jos projekti on monorepo, tarkistaisin packagejen väliset riippuvuudet.
➠ Jos generated-koodia on paljon, katsoisin sen vaikutuksen erikseen.
➠ Ja jos tsc itsessään on nopea, lopettaisin TypeScriptin tutkimisen ja siirtyisin seuraavaan build-vaiheeseen.
➠ Tämä viimeinen kohta on tärkeä.
➠ Jos ongelma ei ole TypeScriptissä, sitä ei myöskään korjata TypeScriptillä.
Buildin ei tarvitse olla mahdollisimman nopea
Tavoite ei oikeastaan ole saada TypeScript-buildiä niin nopeaksi kuin mahdollista.
Tavoite on saada se riittävän nopeaksi ilman, että samalla luovutaan sellaisista tarkistuksista, joita projekti tarvitsee.
Jos esimerkiksi viiden sekunnin säästö tarkoittaa sitä, että type-checkingiä ohitetaan huomattavasti enemmän, en pitäisi sitä automaattisesti hyvänä kauppana.
Kehittäjän kannalta paljon tärkeämpää voi olla se, että pieni muutos voidaan tarkistaa nopeasti ja että CI antaa luotettavan tuloksen.
Siksi buildin optimointi kannattaa nähdä enemmän projektin rakenteen kuin yksittäisen compiler-asetuksen kysymyksenä.
Kun TypeScript-projekti kasvaa, sen ei pitäisi vain saada lisää koodia ympärilleen. Myös rajojen, riippuvuuksien ja build-prosessin pitää pysyä järkevinä.
Siinä vaiheessa build-ajan hidastuminen ei ole enää pelkkä TypeScript-ongelma.
Se on merkki siitä, että projekti alkaa maksaa kasvustaan enemmän kuin ennen.