Hejt Electron.js — analiza przypadku
22.08.2018 22:49
Wojenki pomiędzy zwolennikami różnych języków programowania są bardzo częste. Nie trzeba szukać specjalnie długo, aby znaleźć tonę komentarzy o tym, dlaczego język A jest lepszy od języka B, lub na odwrót. Nie jest to złe. Dzięki przeczytaniu takiej dyskusji początkujący wie, czego warto się obecnie uczyć. Zwykle krytyką obrywały rozwiązania mniej wydajne, mające jakieś poważne wady. Obecnie takim „chłopcem do bicia” stał się Javascript i wszelkie frameworki na nim bazujące, takie jak Electron. Kolejne miejsce na podium „hejterów” zajmuje silnik do tworzenia gier – Unity. Obydwie powyższe technologie zacząłem niedawno poznawać i urzekły mnie nie tylko swoją prostotą, ale także wieloma innymi cechami.
Wbrew tytułowi, wpis nie będzie poświęcony tylko frameworkowi Electron. Ale to narzekanie na aplikacje napisane właśnie w nim zachęciło mnie do napisanie tego krótkiego artykułu.
Krytyka leje się zewsząd, ale czy ma ona sens? Nowe języki programowania, nowe frameworki, nowe rozwiązania są opracowywane po to, aby programy tworzyło się łatwiej, szybciej i przyjemniej. To małe podsumowanie stanowi porównanie starszych datą języków programowania i bibliotek, a także najnowszych trendów.
Do walki stają:
C/C++- połączyłem je w jedną kategorię, gdyż C++ jest de facto rozszerzeniem języka C. Jest to najstarszy język programowania w tym zestawieniu
Java - Android oraz frameworki/silniki (m.in. LibGDX)
Javascript - czysty Javascript nie ma wielkich możliwości - toteż będą brane pod uwagę także frameworki: React.js, Electron.js (przyczyna) oraz ... Unity :)
Wieloplatformowość
Wieloplatformowość od zawsze była zmorą. Każde urządzenie, każda platforma charakteryzuje się swoimi unikalnymi właściwościami. Czego innego wymagamy od komputera, czego innego od telefonu, a czego innego od innych urządzeń. Jaki problem istnieje w tym kontekście? Programowanie natywne (Java ->Android, C/C++->Windows/Linux itd.) to dużo większe koszty. Nie możemy napisać jednej aplikacji, która będzie działała wszędzie. Program na każdą platformę musi być pisany oddzielnie. Ciężko będzie nam znaleźć jakieś fragmenty kodu, które będziemy mogli współdzielić między platformami. Wprowadzenie każdej dodatkowej funkcji to x razy więcej pracy. Dodatkowo, trudno być specjalistą we wszystkim. Tworząc aplikację, najprawdopodobniej będziemy potrzebowali oddzielnego specjalisty od każdej platformy.
Platformy takie jak Electron.js, React.js, oferują nam łatwą do osiągnięcia wieloplatformowość. Owszem, nie ma róży bez kolców. Jest to okupione pewnymi wadami. Nie zmienia to faktu, że koszt poniesiony na opracowanie wieloplatformowej aplikacji w tego typu narzędziach jest o wiele mniejszy. Wprowadzanie zmian również jest o wiele prostsze. Mamy tylko jeden kod źródłowy, który działa na każdej platformie wspieranej przez dany framework. Wymagane do wprowadzenia zmiany są co najwyżej kosmetyczne.
Łatwość programowania
C/C++ jest fajnym językiem. Programuję w nim hobbystycznie różne mniejsze lub większe rzeczy od jakichś 7 lat. Problemem dla początkującej osoby, która szybko chce osiągnąć jakiś konkretny efekt jest to, że często trzeba walczyć z problemami, które nie istnieją w innych, „bardziej wysokopoziomowych” językach. Ot – choćby wskaźniki, na których nie raz mogłem się przejechać. Ich zastosowanie jest co prawda logiczne. Jak zrozumiesz ich zasadę działania, możesz zrobić praktycznie wszystko. Dokładając do tego dynamiczne zarządzanie pamięcią (malloc/new i free/delete) jesteśmy praktycznie w niebie. Nie zmienia to faktu, że w gąszczu tych wspaniałych możliwości technicznych bardzo łatwo o pomyłkę, której znalezienie niejednokrotnie może okazać się niezwykle ciężkie. Zapomnienie zwolnienia pamięci jest poważnym błędem, ale ostatecznie nie spowoduje wykrzaczenia naszej aplikacji. Gorszym problemem jest zwolnienie pamięci a następnie ciągłe używanie wskaźnika wskazującego na tę pamięć. Innym uciążliwym tematem jest przekazywanie parametrów do funkcji przez wskaźnik. Czasami można się omylić i dać nieodpowiednią ilość „gwiazdek”, a następnie się zastanawiać, dlaczego funkcja nie zmienia wartości zmiennej, mimo iż powinna.
Java – moje doświadczenie to głównie tworzenie prostych aplikacji na Androida. Nauka programowania w Javie jest wg mnie trochę prostsza. Co prawda, od razu wchodzimy w świat programowania obiektowego. Ale za to nie musimy martwić się o wiele rzeczy. Nie obawiamy się tego, że przypadkowo odwołamy się do nieistniejącego elementu tablicy (dostaniemy wtedy wyjątek ArrayIndexOfBoundsException). Nie pilnujemy tego, że zaalokowaliśmy pamięć i zapomnieliśmy jej zwolnić. (Robi to za nas Garbage Collector). Ostatecznie nie musimy przejmować się tym, w jaki sposób przekazać argument do metody. Nie mamy dostępnych x metod. Jest tylko jedna – przez referencję.
No i Javascript – znajdował się zawsze w cieniu wielkich potęg. Opracowany przez Netscape’a, początkowo służył tylko do „obróbki” strony internetowej po stronie klienta. Dzięki node.js i frameworkom typu React.js stał się jednym z kluczowych języków do tworzenia nie tylko frontendu, ale także backendu. Za Javascriptem idzie jeszcze większa łatwość nauki programowania. Nie mamy typów zmiennych, (int, float itd.). Argument może być nie tylko liczbą czy obiektem, ale także metodą (w C/C++ też możemy osiągnąć ten efekt, ale musimy bawić się ze wskaźnikami na funkcję. W Javascripcie działa to out‑of-the-box). Korzystając z frameworków pokroju Express.js, czy React.js mamy do dyspozycji także ogromną bazę gotowych do wykorzystania bibliotek. Odpowiada za to menedżer pakietów npm. Microsoft posiada coś podobnego (tam nazywa się to nuget), ale praktycznie nigdy nie miałem potrzeby aby z tego korzystać.Za to korzystanie z npm to czysta przyjemność.
Jeśli chodzi o tworzenie gier, mamy do wyboru dość sporo rozwiązań. Dużą wadą C/C++ jest to, że ichniejsze biblioteki są najczęściej jedno-platformowe. SDL,SFML, Allegro – nie pozwalają nam w łatwy sposób na wygenerowanie np.: mobilnej wersji naszej aplikacji. Z programami okienkowymi nie jest wiele lepiej. Gtk pozwala nam najwięcej na przeniesienie aplikacji z Linuksa na Windowsa. O platformach mobilnych możemy właściwie zapomnieć.
Sprawa wygląda o wiele lepiej w Javie. Mamy do dyspozycji sporo frameworków, które pozwalają nam tworzyć wieloplatformowe aplikacje. Jest to dyspozycji spora gama silników pozwalających na tworzenie gier, takich jak Libgdx czy AndEngine. Aby aplikacja napisana w Libgdx działała na PC, użytkownik nadal potrzebuje zainstalowanej maszyny wirtualnej Java, co stanowi drobny minus.
A co Javascript oferuje nam w sferze tworzenia gier? Mamy do dyspozycji wspaniałe, pełne wszelakich możliwości środowisko Unity, gdzie Javascript jest jednym z dostępnych języków do pisania skryptów. Unity pozwala na tworzenie gier nie tylko na PC i platformy mobilne, ale także na konsole. Jest to atut nie do odrzucenia dla początkującego programisty.
Aplikacje napisane w Javascripcie, w przeciwieństwie do Javy, nie wymagają żadnych dodatkowych składników. Do ich otworzenia wystarczy zwykła przeglądarka internetowa, którą posiada każdy. Nie zaprzeczysz, że to jest wieloplatformowość w najlepszym możliwym wydaniu, prawda?
Wydajność działania vs wydajność programowania
Popularne przysłowie mówi, że jak coś jest do wszystkiego, to jest do niczego. Jest w tym ziarenko prawdy. Frameworki czy silniki wieloplatformowe zwykle odstają wydajnością od rozwiązań natywnych. Dlaczego?
C/C++ - kod źródłowy napisany w tych językach kompilowany jest bezpośrednio do kodu maszynowego, czyli takiego, który procesor może od razu wykonać. Dodatkowo kompilator może zoptymalizować kod wynikowy, aby lepiej wykonywał się na konkretnym procesorze. Racja. Osiągniemy duża szybkość. Ale tracimy przenośność. Program skompilowany pod Windowsa x86 nie uruchomi się na Linuksie, nie mówiąc o innych platformach, takich jak konsole czy smartfony.
W przypadku Javy sytuacja wygląda nieco inaczej. Kod napisany przez programistę program jest kompilowany do tzw. byte-codu. To coś pośredniego między tym, co rozumieją programiści a tym, co rozumie komputer. System operacyjny nie potrafi sam z siebie wykonać poleceń zawartych w byte-codzie. Aby program się uruchomił, potrzebny jest „interpreter” który będzie wiedział, o co w tym byte-codzie chodzi. W przypadku Javy taką rolę odgrywa JVM (Java Virtual Machine). W ramach niej działa między innymi „kolekcjoner śmieci” (Garbage Collector) który dba o to, aby nieużywana pamięć została zwolniona. Ona tłumaczy także byte-code na kod natywny, zrozumiały dla danego procesora. To właśnie dzięki temu Java jest wieloplatformowa. Wystarczy, aby na danej platformie dostępna była wirtualna maszyna Javy. Wtedy będzie można uruchomić tam każdą istniejącą aplikację napisaną w Javie.
Podobna jest sprawa z Javascriptem. W tym wypadku rolę interpretera odgrywa nie jakaś specjalna, dedykowana aplikacja, a sama przeglądarka internetowa. Każda przeglądarka ma wbudowany silnik do interpretacji kodu Javascript (np.: Chakra w przypadku Microsoft Edge czy VP8 w przypadku Google Chrome). Dzięki temu „podzespołowi” deweloperzy mogą stworzyć nie tylko pięknie wyglądające strony internetowe, ale także pełnoprawne aplikacje.
Rozwiązania interpretowane mają pewną wadę. Jak możesz się domyślić, jest to dodatkowa „warstwa abstrakcji” – dodatkowa praca, którą musi wykonać procesor, zanim zostanie wykonany właściwy kod. Ale czy to aż tak bardzo wpływa na wydajność przy współczesnych komputerach? Nie. To jest jeden z mitów, które są dosyć często powielane.
Dodatkową wadą takich aplikacji/języków/frameworków jest zwykle duże zużycie pamięci RAM i miejsca na dysku. Kiedyś obrywała za to Java. Teraz obrywa Javascript i niektóre frameworki, takie jak Electron.js. Dlaczego?
Garbage Collector w Javie jest uruchamiany co jakiś czas. Nie zawsze musi od razu zwolnić nieużywaną pamięć. Czasami takie narzędzia mogą sobie także zaalokować trochę RAM‑u „na zaś”, co oczywiście objawia się dużym zużyciem pamięci w menedżerze zadań.
W Electron.js przyczyna ramożerności/dyskożerności jest nieco inna. Każda aplikacja napisana przy pomocy Electron.js zawiera w sobie całe środowisko niezbędne do jej uruchomienia. Tym środowiskiem jest przeglądarka internetowa i wszystkie od niej komponenty. Uruchamiając aplikację napisaną w Electronie, uruchamiamy tak naprawdę przeglądarkę, która jest niejako „systemem operacyjnym” dla tej aplikacji.
Fakt ten jest zabójczy dla prostych aplikacji pokroju Hello World. Przykładowo, Hello World w C zajmie 100kb miejsca na dysku, a w Electronie 50MB. Jeśli jednak napiszemy bardziej skomplikowaną aplikację, np.: odtwarzacz wideo, zajmie on w C 20MB, a w Electronie nieco ponad 50MB. Dzieje się tak naprawdę, gdyż aplikacja elektronowa ładuje do pamięci kompletną przeglądarkę ze wszystkimi funkcjonalnościami nawet wtedy, gdy ich nie używamy.
Ale czy to jest złe? Nieużywany RAM to RAM zmarnowany, który tylko żre nam prąd, nie dając nic w zamian. Poza tym, pamięć RAM jest w obecnych czasach tania jak barszcz. Przeciętny laptop ma od 4GB do 8GB pamięci RAM. W miarę nowym PC nie ma problemu z umieszczeniem od 8 GB RAM wzwyż. Czy wobec tego „ramo żerność” aplikacji jest aż takim dużym minusem?
Nic nie stoi na przeszkodzie, aby również dzisiaj tworzyć aplikację w asemblerze. Wszak – to właśnie to będzie najwydajniejsze, gdyż na tym poziomie programista ma kontrolę absolutnie nad każdym zdarzeniem w procesorze. Ale czy wyobrażasz sobie, ile czasu zajęłoby programistom stworzenie w asemblerze gry pokroju Wiedźmina 3? No właśnie …
Integracja z systemem operacyjnym
Skupmy się teraz nad aplikacjami webowymi, gdyż to ich głównie dotyczy ten problem. Takie aplikacje (np.: napisany w Electronie Visual Studio) działają tak jakby wewnątrz przeglądarki internetowej. Oznacza to, że tworząc taki program jesteśmy ograniczeni nie interfejsem programowania systemu, a tym, co udostępnia nam przeglądarka internetowa. Tworząc wieloplatformową aplikację przy użyciu takich narzędzi jak Electron czy React.js musimy liczyć się z faktem, że możemy mieć problem z obsługą takich rzeczy jak powiadomienia systemowe, czy zintegrowanie stylu naszej aplikacji ze stylem danego systemu operacyjnego.
Języki takie jak Java w przypadku Androida, czy C/C++ w przypadku PC pozwalają nam "bezpośrednio" gadać z systemem operacyjnym. Mamy bezpośredni dostęp np.: do czujników, GPS‑u i innych modułów w telefonie. Frameworki takie jak React.js czy Electron.js utrudniają taki dostęp, gdyż stanowią warstwę pośrednią między nami (Javascriptem) a systemem operacyjnym. (Np.: Google Chrome nie udostępnia odpowiedniego API, które umożliwiłoby dostęp do modułu NFC w telefonie. Natywnie można to wykonać bez problemu).
Pewnym kluczem do pokonania tego problemu jest ReactNative, który wykorzystuje „natywne” komponenty do wyrenderowania aplikacji. ReactNative pozwala także na połączenie kodu natywnego z kodem JS‑owym. Dzięki temu, jeśli nawet nasza aplikacja będzie wymagała dostosowania niektórych elementów do właściwości danej platformy, to nadal duża część kodu będzie wspólna dla całego projektu.
Postęp?
Postęp idzie do przodu. Różne dzienniki informacyjne alarmują, że specjalistów cały czas brakuje mimo, iż poziom wymaganych umiejętności stale się zmniejsza. Łatwość tworzenia wieloplatformowych aplikacji musi być czymś okupiona. Ale wydaje mi się, że nieco obniżona wydajność nie jest zbyt drogą ceną wobec faktu, że oprogramowanie może być tworzone szybciej, a jego wyprodukowanie będzie kosztowało mniej.