Rust voor web ontwikkeling? De case for webassembly met Rust.
Een tijdje geleden sprak ik met een bevriende ontwikkelaar over webontwikkeling. Ik was iets aan het ontwikkelen in React en hij noemde het gebruik van webassembly. Ik moest toegeven dat ik webassembly helemaal was vergeten. Maar het wekte wel weer mijn nieuwsgierigheid. Toen C# voor het eerst op de markt werd geïntroduceerd, dacht ik dat het een voorbijgaande bevlieging zou zijn, dus ik investeerde er niet veel tijd in en miste de boot. Ik zou dat niet laten gebeuren met webassembly. Dus ik begon wat onderzoek te doen. Maar zou ik C# gebruiken, of zou ik iets anders gebruiken om webassembly te proberen? Ik ging naar YouTube en vond mensen die enthousiast waren over Rust. Maar is Rust wel een goede keuze voor webontwikkeling?
Rust is een taal die gespecialiseerd is in snelheid en geheugenveiligheid. Het doet dit door een aantal zeer strikte regels te gebruiken over welke code “bezit” heeft van en toegang tot bepaalde geheugenlocaties en hoe dat eigenaarschap van de ene naar de andere plek gaat. Het heeft ook vergelijkbare regels voor waar data naartoe kan, hoe het gedeeld wordt, welke code iets kan wijzigen, hoe lang data gegarandeerd kan worden, etc., etc.
Door al deze regels te gebruiken, kan Rust claimen geheugenveilig te zijn zonder dat het een garbage collector nodig heeft, zoals bijvoorbeeld C#. En omdat het zo’n garbage collector niet (nodig) heeft, kan het oogverblindend snel zijn. Het kan zelfs sneller zijn dan veel andere native talen zoals C++. Bovendien zijn er een aantal interessante web development frameworks voor Rust, zoals Yew.
Voor iedereen die wat ontwikkeling heeft gedaan met React, zal Yew waarschijnlijk op het eerste gezicht heel vertrouwd aanvoelen. Dat komt omdat Yew een andere coole feature van Rust gebruikt: de extreem krachtige macro-functies, waarmee je je eigen code kunt gebruiken tijdens het compileren. Ja, je leest het goed! Je kunt zelfs een macro in Rust definiëren die wat input leest en legale Rust-code produceert. Dat betekent dat je HTML-code native in Rust kunt opnemen door de html! macro in Yew te gebruiken.
Geweldig! Laten we allemaal overstappen op Rust en Yew!
Wow, rustig aan.. Dit is misschien allemaal heel cool, maar het betekent niet of Rust ook een goede keuze is voor web ontwikkeling of niet. Ik heb ontdekt dat Rust wel wat nadelen heeft. Het is misschien heel snel, maar heeft web echt al deze extreme snelheidsoptimalisatie nodig? En tegen welke prijs krijg je dit voordeel? In de 3 weken ongeveer dat ik met Rust heb geëxperimenteerd, vond ik het niet heel makkelijk om iets gedaan te krijgen. Vooral bij het delen van data tussen meerdere stukjes code, als je een callbackfunctie wilt maken om een gebeurtenis af te handelen (zoals een muisklik of sleepbewerking) waarbij je wat data moet bijwerken op een object dat buiten die callbackfunctie is gedeclareerd, of wanneer je toegang nodig hebt tot andere data waarvan (volgens de code) niet gegarandeerd kan worden dat ze op dat moment bestaan, dan loop je tegen lastige problemen aan.
Wees u ervan bewust dat dit mijn persoonlijke en professionele mening is, en dat er misschien mensen zijn die geweldige webassembly pagina’s kunnen maken met Rust en Yew, of andere Rust web development frameworks. Maar voor mij voelt het alsof het nog niet helemaal klaar is voor commercieel gebruik. Ik denk dat de vereiste inspanning, energie en tijd op dit moment de extra prestatie of geheugenveiligheid niet waard zijn. Een garbage collector biedt vergelijkbare veiligheid en ik denk dat de prestatiehit van een garbage collector de verminderde inspanning van het leren van de taal en het leren van alles behalve het echt triviale in Rust/Yew aan het werk te krijgen, meer dan waard is.
Dingen zoals callbacks in een struct die iets in ‘self’ willen updaten, kunnen uren duren om uit te vinden vanwege Rust’s regels over levensduur, lenen, verplaatsen, kopiëren, etc. Zelfs als je Rc<RefCell<Self>> gebruikt, kan het nog steeds moeilijk zijn of aanvoelen als ronduit onmogelijk om iets te laten werken vanwege fouten als ‘self ontsnapt hier aan de methodebody’ of zo iets vaags. Laat me duidelijk zijn dat Rust uitstekend werk levert als het gaat om foutrapportage. Waarschijnlijk het beste dat ik tot nu toe heb gezien. Maar ik denk niet dat dat veel helpt als na meerdere uren proberen om een callbackfunctie te laten werken die gegevens buiten die functie update, het nog steeds niet werkt, de code een vreselijke puinhoop is geworden of het werkt terwijl er veel obscure code wordt gebruikt met Rc<RefCell<T>> (wat ten slotte toch weer terugvalt op het gebruik van onveilige code om de mutable sharing te laten werken). Zulke code maakt het moeilijk om te lezen en kan onmogelijk en onlogisch aanvoelen. Dit werkt bijvoorbeeld wel in Rust:
let divclone = Rc::clone(&self.div);
let div = divclone.borrow_mut();
let div = div.as_ref().unwrap();
Maar als je deze code probeert in te korten door de laatste twee regels samen te voegen, werkt het niet meer:
let divclone = Rc::clone(&self.div);
let div = divclone.borrow_mut().as_ref().unwrap();
// Dit geeft een fout omdat de tijdelijke waarde van divclone.borrow_mut()
// buiten bereik raakt bij de puntkomma aan het einde van de regel, maar dat is nodig om
// de legale waarde van de div-variabele te garanderen.
// Maar zelfs bij het opnieuw definiëren van de div-variabele in de laatste regel van het werkende voorbeeld,
// gaat die tijdelijke waarde blijkbaar niet buiten bereik, wat (in ieder geval voor mij) totaal onlogisch lijkt.
De voorbeelden, of het nu gaat om rust, yew, wasm_bindgen, web_sys of iets dergelijks, schieten ernstig tekort in dit aspect. Ze zijn verouderd en laten veel veelvoorkomende belangrijke usecases niet zien. Een eenvoudig use case in yew met een closure in een struct component waarbij je zowel naar ‘self’ moet verwijzen EN iets wilt updaten in de context die is gedeclareerd op het app component level, kan onmogelijk te achterhalen zijn voor een beginner. Dit is demotiverend, demoraliserend en vervelend.
Dezelfde kritiek die ik heb op Angular (meer dan de helft van je tijd moeten besteden aan code voor het framework), heb ik ook op Rust. Het kost gewoon te veel tijd en moeite om de krankzinnig ingewikkelde regels van Rust door te werken om het gebruik ervan voor het maken van een commerciële webapplicatie te rechtvaardigen. In het geval van het gebruik ervan voor kernelontwikkeling, begrijp ik het gebruik van Rust absoluut en onderschrijf ik het volledig. Maar waarom zou ik Rust willen gebruiken voor het maken van een webpagina die me drie dagen kost, een enorme hoofdpijn oplevert en het grootste deel van mijn tijd besteedt aan het bekijken van compilerfouten, stackoverflow, voorbeelden en documentatie van Rust, en zeer ondoorzichtige code, om een webapp te maken die alleen een teller verhoogt wanneer ik op een knop druk? (Iets waar een serieuze zakelijke klant nooit om zal vragen.)
Natuurlijk weet ik dat die voorbeelden slechts bouwstenen en demonstraties van principes zijn en dat er makkelijkere manieren zijn om een teller te laten werken dan door een struct te gebruiken met een rendermethode die de DOM manipuleert en de tellerwaarde opslaat in de gegevens van de struct. Maar dit is slechts een voorbeeld. Ik doe dit niet als hobby. Uiteindelijk zullen er gegevens zijn die ik in de struct moet opslaan. Ik wil iets groters kunnen maken dat ik kan verkopen, wat betekent dat ik mijn eigen microarchitecturen moet kunnen maken en dat ik meer moet kunnen doen dan alleen een teller verhogen wanneer de gebruiker op een knop klikt. Ik wil verdeelbalken tussen gebieden kunnen slepen, panelen kunnen slepen en ergens kunnen vastzetten, scrollen tussen twee scrollbare gebieden kunnen synchroniseren, etc. Maar zelfs in Yew wordt het standaard sleepgedrag van een component niet overschreven, wat betekent dat wanneer u de gebeurtenissen buttondown, mousemove en buttonup implementeert om een versleepbare verdeelbalk te implementeren, het slechts in ongeveer 25-75% van de gevallen zal werken wanneer u het probeert te slepen. In andere gevallen zal de browser de standaard sleepactie starten. Dit is een probleem dat ik niet had in React, dus ik had het niet verwacht. In Yew ontdekte ik na meerdere dagen debuggen en in wanhoop uiteindelijk hetzelfde te implementeren in gewoon javascript en vervolgens hetzelfde probleem te krijgen, dat ik een dragstart event handler moest implementeren om het standaardgedrag te voorkomen.
Yew helpt een heel eind met zijn html! macro, maar ik vind de focus op functiecomponenten boven structcomponenten niet fijn (een persoonlijke voorkeur; en ja, ik vind hetzelfde niet fijn aan React). Ik denk dat dit een tendens is die is overgewaaid van javascript en het is volgens mij geen goede ontwikkeling omdat het gebruik van structs in Rust veel krachtiger is als het goed wordt gebruikt. Toch kan Yew een handig contextobject bieden om informatie te verstrekken die van toepassing is op alle componenten (zoals stijlinformatie, wat handig is als je bijvoorbeeld darkmode en lightmode wilt aanbieden), maar na meerdere dagen zoeken, experimenteren en zelfs met de hulp van ChatGPT, kon ik geen manier vinden om wat gegevens in de context te wijzigen (zoals een menu-item om te wisselen tussen darkmode en lightmode), anders dan een callback-eigenschap door meerdere lagen componenten te tunnelen. Dit is precies waarvoor het contextobject in het leven is geroepen, zodat het erg vreemd lijkt dat het wijzigen van de context toch weer door meerdere lagen componenten heen via event properties moet.
Now, sure, you could argue that the performance and stability gains will always stay while I only need to write the source code once.. But as we’ve seen in the real world: commercial (web) applications are always in motion. Vendors always keep changing the code and adding new functionality, rewriting parts of the product, etc..
Entropy in source code
Pragmatische Dave Thomas en Andrew Hunt schreven ooit over code-entropie. Code in ontwikkeling wordt na verloop van tijd rommeliger. Commentaar heeft de neiging om af te drijven van de code waar het betrekking op heeft. Functies hebben soms bijwerkingen die na verloop van tijd worden vergeten, wat toekomstige storingen veroorzaakt. Bugs worden gepatcht onder wat pleisters en omzeild. Rust is niet vrij van dit fenomeen. Gisteravond nog zag ik iemand op rust-lang.org een vraag beantwoorden over een ‘self escapes the method body here’-fout waarbij de programmeur die de vraag stelde blijkbaar een lifetime-specificatie misbruikte om een van de vele regels van Rust te omzeilen. Je zou kunnen stellen dat Rust, met zijn minder dan triviale regels, meer last zou kunnen hebben van code-entropie dan de meeste talen. Dat betekent dat je als bedrijfseigenaar, wanneer je een commercieel webgebaseerd product met Rust ontwikkelt, echt moet begrijpen waarin je investeert.
Vergeet nooit dat het misschien mogelijk is om vandaag iets moois te maken in Rust.. Of in ieder geval dit jaar (ik zou iedereen die beweert dat ze in één dag een commerciële MVP in Rust kunnen maken, serieus willen uitdagen).. Je toekomstige collega’s zullen er heel goed in slagen om het te verpesten. Niet vanwege kwade bedoelingen, maar gewoon omdat code nooit de volledige bedoeling van de programmeur die het schreef, uitlegt. Als de complexiteit van Rust meer van dit soort entropie uitlokt, zou dat er waarschijnlijk toe leiden dat commerciële webapplicaties in Rust snel onmogelijk te onderhouden worden.
Conclusie: Is Rust een goede keuze voor web ontwikkeling?
Als je werkt aan een besturingssysteemkernel, medische applicatie, voertuigcontrolesysteem, astrofysica of kwantumfysica applicatie, high-end computerspel, rekenwerk of iets anders waarbij je echt die extra stabiliteit en prestaties nodig hebt, is het volkomen logisch om Rust te gebruiken. Maar voor webassembly, of elk soort gebruikersinterfacewerk, waarbij je applicatie toch al het grootste deel van de tijd wacht op gebruikersinvoer, zie ik het extra voordeel van Rust niet. Misschien als iemand een taal zou creëren die een soort superset van Rust zou zijn (zoals Typescript dat is voor Javascript), waardoor veel dingen makkelijker en sneller te implementeren zouden zijn, zodat het makkelijker zou zijn om productief te zijn en onderhoudbare code te schrijven die eenvoudig te begrijpen, debuggen, wijzigen en aan toe te voegen is, dan zou het misschien een interessante toekomst kunnen hebben op het web en zal ik het zeker een nieuwe kans geven. Want of je Rust nu leuk vindt of niet, ik denk dat het niet te ontkennen valt dat het een behoorlijk geweldige technologie is, en (naar mijn bescheiden mening) een van de belangrijkste innovaties in softwareontwikkeling van de afgelopen 20 jaar. Maar op dit moment is Rust volgens mij waarschijnlijk geen goede keuze voor web ontwikkeling of voor welke vorm van gebruikersinterfacewerk dan ook.