Kompresja JavaScript oraz kompresja CSS może znacząco przyśpieszyć wczytywanie się strony, co jest szczególnie ważne dla osób korzystających z wolnych łącz (pozdrawiam tutaj wszystkich klientów Asta-Net, którzy płacą ponad 50 zł/m-c i mogą ściągać z prędkością 5 KB/s :D, do których niestety też się zaliczam).
We wpisie tym pokrótce zaprezentuję bardzo ciekawą metodę kompresji plików JavaScript oraz CSS z wykorzystaniem algorytmu deflate (dla plików PNG) oraz technologii Canvas.
Najciekawszy w tym rozwiązaniu jest sposób przechowywania skompresowanych danych. Otóż kod JavaScript, tudzież CSS (czy jakikolwiek inny) jest konwertowany do obrazu PNG (który jak wiadomo jest bardzo dobrze spakowany), co znacząco zmniejsza wielkość pliku.
A więc najpierw pakujemy nasze pliki do PNG. Następnie wczytujemy stronę, odkodowujemy plik PNG (mniejszy o kilkanaście, czasem nawet kilkadziesiąt procent) i wykonujemy po stronie przeglądarki. Prosto, szybko i przyjemnie!
Praktyczny przykład
A więc najpierw musimy użyć jakiegoś narzędzia do kompresji JavaScript (czy CSS). Następnie przystępujemy do zapisu naszych spakowanych plików do formatu PNG. Możemy napisać własny skrypt PHP z wykorzystaniem biblioteki GD, możemy także skorzystać z gotowców.
ruby:
http://gist.github.com/542462
php:
http://github.com/iamcal/PNGStore
Wygenerowany w ten sposób plik może wyglądać następująco:
- jquery-1.2.3.min.js (17 KB)
- prototype-1.6.0.2.js (30 KB)
Jak więc widać, jest to zwykły plik PNG możliwy do otwarcia w dowolnej przeglądarce graficznej.
Następnym krokiem jest zamieszczenie prostej funkcji na naszej stronie, który zajmie się „odkodowaniem” stworzonego pliku PNG:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | /** * Load PNG Data * Source: http://www.nihilogic.dk/labs/canvascompress/pngdata.js **/ function loadPNGData(strFilename, fncCallback) { // test for canvas and getImageData var bCanvas = false; var oCanvas = document.createElement("canvas"); if (oCanvas.getContext) { var oCtx = oCanvas.getContext("2d"); if (oCtx.getImageData) { bCanvas = true; } } if (bCanvas) { var oImg = new Image(); oImg.style.position = "absolute"; oImg.style.left = "-10000px"; document.body.appendChild(oImg); oImg.onload = function() { var iWidth = this.offsetWidth; var iHeight = this.offsetHeight; oCanvas.width = iWidth; oCanvas.height = iHeight; oCanvas.style.width = iWidth+"px"; oCanvas.style.height = iHeight+"px"; var oText = document.getElementById("output"); oCtx.drawImage(this,0,0); var oData = oCtx.getImageData(0,0,iWidth,iHeight).data; var a = []; var len = oData.length; var p = -1; for (var i=0;i<len;i+=4) { if (oData[i] > 0) a[++p] = String.fromCharCode(oData[i]); }; var strData = a.join(""); if (fncCallback) { fncCallback(strData); } document.body.removeChild(oImg); } oImg.src = strFilename; return true; } else { return false; } } |
Funkcja ta przyjmuje dwa parametry: ścieżkę do obrazu PNG oraz funkcję callback, która zostanie wywołana po prawidłowym załadowaniu obrazu. Tak może wyglądać nasz przykład:
1 2 3 4 5 6 7 8 9 | loadPNGData('jquery.min.js.png', function(d) { eval( d ); $(function() { alert('its work!'); }); }); |
Co tutaj się dzieje? Najpierw odbieramy nasz „obrazek” (w którym jest zakodowana biblioteka jQuery) przy pomocy funkcji loadPNGData, tworzymy nowy obiekt Canvas, operując na obrazie odkodowujemy piksel po pikselu zakodowany kod, przekazujemy go do funkcji callback, wykonujemy kod przy pomocy funkcji eval (jest to jednoznaczne z „wczytaniem” biblioteki jQuery) – no i od tej pory możemy korzystać z załadowanego skryptu.
Prawda, że proste? :-) Równie dobrze w takim zakodowanym pliku może być kod CSS, dla którego należałoby stworzyć nowy element STYLE w drzewie DOM dokumentu i go tam zagnieździć, co też nie jest problemem.
Zalety
Główną zaletą, o której już wcześniej wspominałem, jest spadek wielkości skompresowanego pliku o kilkanaście, a czasem nawet kilkadziesiąt procent.
Jak działa taka kompresja? Otóż wyczytałem, że w plikach PNG jest stosowana kompresja DEFLATE (pisałem co nieco o kompresji przy pomocy mod_deflate). Ten sam algorytm jest wykorzystywany przez GZIP oraz kompresję HTTP.
A więc kolejną zaletą jest możliwość korzystania z kompresji deflate, nie zważając na to, czy przeglądarka obsługuje GZIP, czy na serwerze nie zostało zablokowane wysyłanie skompresowanych plików (lub po prostu nie zainstalowano modułu mod_deflate).
Wady
Niestety powyższa kompresja ma jedną zasadniczą wadę: nie wszystkie przeglądarki obsługują obiekt Canvas i metodę getImageData. Jeśli nie obsługują to nasze pliki wcale nie zostaną wykonane.
A najgorsze w tym, że Canvas jako dość nowa technologia nie jest wspierana przez niektóre przeglądarki oraz urządzenia (zwłaszcza starszy software oraz przeglądarki mobilne).
Kolejną wadą jest konieczność „odkodowania” pliku PNG przez JavaScript (zajmuje się tym funkcja loadPNGData), co niestety jest dość obciążające dla procesora naszego klienta. Myślę, że będzie to mniej wydajnym rozwiązaniem, aniżeli rozpakowanie pliku przez przeglądarkę (gdyby został spakowany po stronie serwera w GZIP).
Podsumowanie
Chciałem w tym wpisie przedstawić krótko bardzo fajną ciekawostkę – dla mnie to coś zupełnie nowego i interesującego. Choć nie jest to rozwiązanie odpowiednie do komercyjnych aplikacji, jednak dobrze znać taki sposób kompresji plików JavaScript czy CSS.
To jest na prawdę szalone :D
Interesujące, ale wady zabijają plusy. Najprościej włączyć gzip, która przeglądarka tego dziś nie obsługuje?
Pewnie każda przeglądarka, która pozwala wykorzystać technologię Canvas ma także zaimplementowane algorytmy gzip :-) Już prędzej można spotkać prymitywne serwery, na których jest wyłączony gzip i nie możemy z niego skorzystać (republika.pl, zapewne webpark, darmowe serwery i inne).
Bardzo ciekawe :)
Już odbiegając od tego, że canvas nie jest zaimplementowany w IE < 9, ale tak jak piszesz wykorzystanie funkcji eval() jest jednym z najmniej optymalnych rozwiązań. Natomiast to też zależy jakie priorytety mamy, bo jeśli chodzi nam głównie o to aby nie zapychać łącza a nie o to żeby szybciej się wykonały kod po stronie klienta to rozwiązanie jest jak najbardziej okej :)
To rozwiązanie zostało opracowane, jeśli się nie mylę, na potrzeby jakiegoś konkursu typu 10k apart. W tym konkretnym wypadku miało sens, ponieważ, jak się domyślam, gzip nie był brany pod uwagę.
Jeśli jednak gzip daje taki sam zysk jak kompresja do PNG, to nie widzę sensu używać tego drugiego rozwiązania. Jest niekompatybilne wstecz, nieładne i zapewne dosyć toporne jeśli chodzi o wydajność.
Ale sam pomysł ryje beret :)
Zgadza się, to rozwiązanie jest zwykłą ciekawostką, aczkolwiek w konkursach gdzie nikt nie patrzy na wydajność czy wygląd kodu, a liczy się jedynie jak najmniejszy rozmiar (5K Apart, 10K Apart) to takie rozwiązanie nadaje się świetnie :-)