Smart Bundling: jak udostępniać starszy kod tylko starszym przeglądarkom
Opublikowany: 2022-03-10Strona internetowa otrzymuje obecnie dużą część ruchu z wiecznie zielonych przeglądarek — z których większość ma dobrą obsługę ES6+, nowe standardy JavaScript, nowe interfejsy API platform internetowych i atrybuty CSS. Jednak starsze przeglądarki nadal muszą być obsługiwane w najbliższej przyszłości — ich udział w użyciu jest na tyle duży, że nie można ich zignorować, w zależności od bazy użytkowników.
Szybkie spojrzenie na tabelę użycia caniuse.com pokazuje, że wiecznie zielone przeglądarki zajmują lwią część rynku przeglądarek — ponad 75%. Mimo to normą jest przedrostek CSS, transpilacja całego naszego JavaScriptu do ES5 i dodawanie wypełniaczy, aby wspierać każdego użytkownika, na którym nam zależy.
Chociaż jest to zrozumiałe z kontekstu historycznego — w sieci zawsze chodziło o stopniowe ulepszanie — pozostaje pytanie: czy spowalniamy sieć dla większości naszych użytkowników, aby obsługiwać zmniejszający się zestaw starszych przeglądarek?
Koszt obsługi starszych przeglądarek
Spróbujmy zrozumieć, jak różne kroki w typowym potoku kompilacji mogą zwiększyć wagę naszych zasobów frontonu:
Transpilacja do ES5
Aby oszacować, ile wagi transpilacja może dodać do pakietu JavaScript, wziąłem kilka popularnych bibliotek JavaScript napisanych pierwotnie w ES6+ i porównałem ich rozmiary przed i po transpilacji:
Biblioteka | Rozmiar (zminimalizowany ES6) | Rozmiar (zminimalizowany ES5) | Różnica |
---|---|---|---|
TodoMVC | 8,4 KB | 11 KB | 24,5% |
Przeciągany | 53,5 KB | 77,9 KB | 31,3% |
Luxon | 75,4 KB | 100,3 KB | 24,8% |
Video.js | 237,2 KB | 335,8 KB | 29,4% |
PixiJS | 370,8 KB | 452 KB | 18% |
Przeciętnie nietranspilowane pakiety są o około 25% mniejsze niż te, które zostały przetranspilowane do ES5. Nie jest to zaskakujące, biorąc pod uwagę, że ES6+ zapewnia bardziej zwarty i ekspresyjny sposób przedstawiania równoważnej logiki, a transpilacja niektórych z tych funkcji do ES5 może wymagać dużo kodu.
Wypełnienia ES6+
Chociaż Babel wykonuje dobrą robotę przy stosowaniu przekształceń składniowych w naszym kodzie ES6+, wbudowane funkcje wprowadzone w ES6+ — takie jak Promise
, Map
i Set
, a także nowe metody tablicowe i łańcuchowe — nadal wymagają wielokrotnych wypełniania. Upuszczenie babel-polyfill
w obecnej postaci może dodać blisko 90 KB do twojego zminimalizowanego pakietu.
Wypełnienia platformy internetowej
Tworzenie nowoczesnych aplikacji internetowych zostało uproszczone dzięki dostępności wielu nowych interfejsów API przeglądarki. Powszechnie używane są fetch
, do żądania zasobów, IntersectionObserver
, do wydajnej obserwacji widoczności elementów oraz specyfikacja URL
, która ułatwia odczytywanie i manipulowanie adresami URL w sieci.
Dodanie wypełnienia zgodnego ze specyfikacją dla każdej z tych funkcji może mieć zauważalny wpływ na rozmiar pakietu.
Prefiks CSS
Na koniec spójrzmy na wpływ prefiksów CSS. Chociaż prefiksy nie dodają tak dużego ciężaru do pakietów, jak robią to inne przekształcenia kompilacji — zwłaszcza, że kompresują się dobrze, gdy Gzip'd — nadal można tu osiągnąć pewne oszczędności.
Biblioteka | Rozmiar (zminimalizowany, prefiks dla ostatnich 5 wersji przeglądarki) | Rozmiar (zminimalizowany, prefiks dla ostatniej wersji przeglądarki) | Różnica |
---|---|---|---|
Bootstrap | 159 KB | 132 KB | 17% |
Bulma | 184 KB | 164 KB | 10,9% |
Fundacja | 139 KB | 118 KB | 15,1% |
Semantyczny interfejs użytkownika | 622 KB | 569 KB | 8,5% |
Praktyczny przewodnik po efektywnej wysyłce kodu
Prawdopodobnie jest oczywiste, dokąd z tym zmierzam. Jeśli wykorzystamy istniejące potoki kompilacji, aby dostarczyć te warstwy zgodności tylko do przeglądarek, które tego wymagają, możemy zapewnić lżejsze środowisko pozostałym użytkownikom — tym, którzy stanowią rosnącą większość — przy zachowaniu zgodności ze starszymi przeglądarkami.
Ten pomysł nie jest całkowicie nowy. Usługi takie jak Polyfill.io próbują dynamicznie wypełniać środowiska przeglądarki w czasie wykonywania. Ale podejście takie jak to ma kilka wad:
- Wybór wypełniaczy jest ograniczony do tych wymienionych przez usługę — chyba że sam hostujesz i utrzymujesz usługę.
- Ponieważ wypełnianie wielokrotne odbywa się w czasie wykonywania i jest operacją blokującą, czas ładowania strony może być znacznie dłuższy dla użytkowników starszych przeglądarek.
- Udostępnianie niestandardowego pliku wypełnienia każdemu użytkownikowi wprowadza entropię do systemu, co utrudnia rozwiązywanie problemów, gdy coś pójdzie nie tak.
Nie rozwiązuje to również problemu wagi dodanej przez transpilację kodu aplikacji, który czasami może być większy niż same wypełnienia.
Zobaczmy, jak możemy rozwiązać wszystkie źródła wzdęć, które do tej pory zidentyfikowaliśmy.
Narzędzia, których będziemy potrzebować
- Pakiet internetowy
To będzie nasze narzędzie do budowania, chociaż proces pozostanie podobny do innych narzędzi do budowania, takich jak Parcel i Rollup. - Lista przeglądarek
Dzięki temu będziemy zarządzać i definiować przeglądarki, które chcemy obsługiwać. - I użyjemy niektórych wtyczek obsługujących Browserslist .
1. Definiowanie nowoczesnych i starszych przeglądarek
Najpierw chcemy wyjaśnić, co rozumiemy przez „nowoczesne” i „starsze” przeglądarki. Aby ułatwić konserwację i testowanie, warto podzielić przeglądarki na dwie odrębne grupy: dodać przeglądarki, które nie wymagają wypełniania lub transpilacji w niewielkim stopniu do naszej nowoczesnej listy, a resztę umieścić na naszej starszej liście.
Konfiguracja Browserslist w katalogu głównym projektu może przechowywać te informacje. Podsekcje „Środowisko” mogą służyć do dokumentowania dwóch grup przeglądarek, na przykład:
[modern] Firefox >= 53 Edge >= 15 Chrome >= 58 iOS >= 10.1 [legacy] > 1%
Podana tutaj lista jest tylko przykładem i może być dostosowywana i aktualizowana w zależności od wymagań Twojej witryny i dostępnego czasu. Ta konfiguracja będzie działać jako źródło prawdy dla dwóch zestawów pakietów front-end, które stworzymy w następnej kolejności: jednego dla nowoczesnych przeglądarek i drugiego dla wszystkich innych użytkowników.
2. ES6 + Transpiling i wypełnianie
Aby transpilować nasz JavaScript w sposób uwzględniający środowisko, użyjemy babel-preset-env
.
Zainicjujmy plik .babelrc
w katalogu głównym naszego projektu w następujący sposób:
{ "presets": [ ["env", { "useBuiltIns": "entry"}] ] }
Włączenie flagi useBuiltIns
umożliwia Babel selektywne wypełnianie wbudowanych funkcji, które zostały wprowadzone jako część ES6+. Ponieważ filtruje wypełniacze, aby uwzględnić tylko te wymagane przez środowisko, zmniejszamy koszty wysyłki w całości z babel-polyfill
.
Aby ta flaga działała, będziemy musieli również zaimportować babel-polyfill
w naszym punkcie wejścia.
// In import "babel-polyfill";
Spowoduje to zastąpienie dużego importu babel-polyfill
granularnymi, filtrowanymi według docelowego środowiska przeglądarki.
// Transformed output import "core-js/modules/es7.string.pad-start"; import "core-js/modules/es7.string.pad-end"; import "core-js/modules/web.timers"; …
3. Funkcje platformy internetowej do wypełniania
Aby dostarczyć naszym użytkownikom wypełnienia dla funkcji platformy internetowej, będziemy musieli utworzyć dwa punkty wejścia dla obu środowisk:
require('whatwg-fetch'); require('es6-promise').polyfill(); // … other polyfills
I to:
// polyfills for modern browsers (if any) require('intersection-observer');
To jedyny krok w naszym przepływie, który wymaga pewnego stopnia ręcznej konserwacji. Możemy uczynić ten proces mniej podatnym na błędy, dodając do projektu eslint-plugin-compat. Ta wtyczka ostrzega nas, gdy korzystamy z funkcji przeglądarki, która nie została jeszcze wypełniona.
4. Prefiks CSS
Na koniec zobaczmy, jak możemy ograniczyć prefiksy CSS dla przeglądarek, które tego nie wymagają. Ponieważ autoprefixer
był jednym z pierwszych narzędzi w ekosystemie wspierającym odczyt z pliku konfiguracyjnego listy browserslist
, nie mamy tutaj wiele do zrobienia.
Utworzenie prostego pliku konfiguracyjnego PostCSS w katalogu głównym projektu powinno wystarczyć:
module.exports = { plugins: [ require('autoprefixer') ], }
Kładąc wszystko razem
Teraz, gdy zdefiniowaliśmy wszystkie wymagane konfiguracje wtyczek, możemy połączyć konfigurację webpacka, która je odczytuje i wyprowadza dwie oddzielne kompilacje w folderach dist/modern
i dist/legacy
.
const MiniCssExtractPlugin = require('mini-css-extract-plugin') const isModern = process.env.BROWSERSLIST_ENV === 'modern' const buildRoot = path.resolve(__dirname, "dist") module.exports = { entry: [ isModern ? './polyfills.modern.js' : './polyfills.legacy.js', "./main.js" ], output: { path: path.join(buildRoot, isModern ? 'modern' : 'legacy'), filename: 'bundle.[hash].js', }, module: { rules: [ { test: /\.jsx?$/, use: "babel-loader" }, { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] } ]}, plugins: { new MiniCssExtractPlugin(), new HtmlWebpackPlugin({ template: 'index.hbs', filename: 'index.html', }), }, };
Na koniec utworzymy kilka poleceń kompilacji w naszym pliku package.json
:
"scripts": { "build": "yarn build:legacy && yarn build:modern", "build:legacy": "BROWSERSLIST_ENV=legacy webpack -p --config webpack.config.js", "build:modern": "BROWSERSLIST_ENV=modern webpack -p --config webpack.config.js" }
Otóż to. yarn build
z biegania powinno teraz dać nam dwie wersje, które są równoważne pod względem funkcjonalności.
Udostępnianie odpowiedniego pakietu użytkownikom
Tworzenie osobnych kompilacji pomaga nam osiągnąć tylko pierwszą połowę naszego celu. Nadal musimy zidentyfikować i udostępnić użytkownikom odpowiedni pakiet.
Pamiętasz konfigurację Browserslist, którą zdefiniowaliśmy wcześniej? Czy nie byłoby miło, gdybyśmy mogli użyć tej samej konfiguracji do określenia, do której kategorii należy użytkownik?
Wpisz przeglądarkę listy-użytkownika. Jak sama nazwa wskazuje, browserslist-useragent
może odczytać naszą konfigurację browserslist
, a następnie dopasować klienta użytkownika do odpowiedniego środowiska. Poniższy przykład ilustruje to z serwerem Koa:
const Koa = require('koa') const app = new Koa() const send = require('koa-send') const { matchesUA } = require('browserslist-useragent') var router = new Router() app.use(router.routes()) router.get('/', async (ctx, next) => { const useragent = ctx.get('User-Agent') const isModernUser = matchesUA(useragent, { env: 'modern', allowHigherVersions: true, }) const index = isModernUser ? 'dist/modern/index.html', 'dist/legacy/index.html' await send(ctx, index); });
Tutaj ustawienie flagi allowHigherVersions
zapewnia, że jeśli zostaną wydane nowsze wersje przeglądarki — te, które nie są jeszcze częścią bazy danych Can I Use — nadal będą zgłaszane jako prawdziwe dla nowoczesnych przeglądarek.
Jedną z funkcji browserslist-useragent
jest zapewnienie, że dziwactwa platformy są brane pod uwagę podczas dopasowywania agentów użytkownika. Na przykład wszystkie przeglądarki w systemie iOS (w tym Chrome) używają WebKit jako podstawowego silnika i zostaną dopasowane do odpowiedniego zapytania Listy przeglądarek specyficznego dla Safari.
Może nie być rozsądne poleganie wyłącznie na poprawności parsowania klienta użytkownika w środowisku produkcyjnym. Wracając do starszego pakietu dla przeglądarek, które nie są zdefiniowane na nowoczesnej liście lub mają nieznane lub niemożliwe do przeanalizowania ciągi agenta użytkownika, zapewniamy, że nasza witryna nadal będzie działać.
Wniosek: czy warto?
Udało nam się zapewnić kompleksowy przepływ pakietów bez rozrostu dla naszych klientów. Ale rozsądne jest zastanawianie się, czy koszty utrzymania, które wnosi to do projektu, są warte korzyści. Oceńmy plusy i minusy tego podejścia:
1. Konserwacja i testowanie
Wymagane jest utrzymywanie tylko jednej konfiguracji Browserslist, która obsługuje wszystkie narzędzia w tym potoku. Aktualizację definicji nowoczesnych i starszych przeglądarek można przeprowadzić w dowolnym momencie w przyszłości, bez konieczności refaktoryzacji obsługiwanych konfiguracji lub kodu. Twierdzę, że to sprawia, że koszty utrzymania są prawie nieistotne.
Istnieje jednak niewielkie teoretyczne ryzyko związane z poleganiem na Babel przy tworzeniu dwóch różnych pakietów kodu, z których każdy musi działać poprawnie w swoim środowisku.
Chociaż błędy wynikające z różnic w pakietach mogą być rzadkie, monitorowanie tych wariantów pod kątem błędów powinno pomóc w identyfikacji i skutecznym łagodzeniu wszelkich problemów.
2. Czas kompilacji a czas działania
W przeciwieństwie do innych powszechnie stosowanych obecnie technik, wszystkie te optymalizacje pojawiają się w czasie kompilacji i są niewidoczne dla klienta.
3. Stopniowo zwiększona prędkość
Doświadczenie użytkowników nowoczesnych przeglądarek staje się znacznie szybsze, podczas gdy użytkownicy starszych przeglądarek nadal otrzymują ten sam pakiet, co wcześniej, bez żadnych negatywnych konsekwencji.
4. Korzystanie z nowoczesnych funkcji przeglądarki z łatwością
Często unikamy korzystania z nowych funkcji przeglądarki ze względu na rozmiar wymaganych do ich użycia wypełnień. Czasami wybieramy nawet mniejsze wypełniacze niespełniające specyfikacji, aby zaoszczędzić na rozmiarze. To nowe podejście pozwala nam używać wypełniaczy zgodnych ze specyfikacją bez martwienia się o to, że wpłynie to na wszystkich użytkowników.
Pakiet różnicowy obsługujący produkcję
Biorąc pod uwagę znaczące korzyści, przyjęliśmy ten plan kompilacji podczas tworzenia nowej platformy mobilnej kasy dla klientów Urban Ladder, jednego z największych indyjskich sprzedawców mebli i dekoracji.
W naszym już zoptymalizowanym pakiecie byliśmy w stanie wycisnąć około 20% oszczędności na zasobach CSS i JavaScript w formacie Gzip, które zostały wysłane do nowoczesnych użytkowników mobilnych. Ponieważ ponad 80% naszych codziennych odwiedzających korzystało z tych wiecznie zielonych przeglądarek, włożony wysiłek był wart tego wpływu.
Dalsze zasoby
- „Ładowanie wypełniaczy tylko wtedy, gdy są potrzebne”, Philip Walton
-
@babel/preset-env
Inteligentne ustawienie Babel - Lista przeglądarek „Narzędzia”
Ekosystem wtyczek zbudowanych dla Browserslist - Mogę uzyć
Aktualna tabela udziału w rynku przeglądarki