DevTips – jak wydajnie kompilować zasoby

Opublikowany: 2022-08-25

Jednym z celów, jakie postawiliśmy sobie na ten rok, było przerobienie naszych dwóch flagowych wtyczek (Nelio Content i Nelio A/B Testing) na typy TypeScript i React Hooks. No cóż, mamy dopiero pół roku i już możemy powiedzieć, że ten cel był całkowitym sukcesem. Muszę jednak przyznać, że droga była nieco bardziej skomplikowana, niż się spodziewaliśmy… zwłaszcza jeśli weźmiemy pod uwagę, że po wprowadzeniu TypeScript, nasz Czas budowy wtyczek skrócił się z kilku sekund do ponad dwóch minut! Coś było nie tak i nie wiedzieliśmy co.

Cóż, w dzisiejszym poście chciałbym opowiedzieć trochę o tym doświadczeniu i co zrobiliśmy, aby to naprawić. W końcu wszyscy wiemy, że TypeScript zawsze nieco spowolni proces budowania (sprawdzanie typu ma swoją cenę), ale nie powinno to być aż tak duże! Cóż, uwaga spoilera: problemem nigdy nie był TypeScript… to była moja konfiguracja. TypeScript uczynił to tylko „oczywistym”. Więc zacznijmy, dobrze?

Projekt zabawkowy

Aby pomóc Ci zrozumieć problem, na który natknęliśmy się kilka tygodni temu, i sposób, w jaki go naprawiliśmy, najlepiej, co możemy zrobić, to stworzyć bardzo prosty przykład do naśladowania. Zbudujmy prostą wtyczkę WordPress, która używa TypeScript i jak błędna konfiguracja może skutkować bardzo długimi czasami kompilacji. Jeśli potrzebujesz pomocy, aby zacząć, sprawdź ten post w środowiskach programistycznych WordPress.

Tworzenie wtyczki

Pierwszą rzeczą, którą powinieneś zrobić, to utworzyć nowy folder z nazwą wtyczki (na przykład nelio ) w katalogu /wp-content/plugins WordPressa. Następnie dodaj plik główny ( nelio.php ) o następującej treści:

 <?php /** * Plugin Name: Nelio * Description: This is a test plugin. * Version: 0.0.1 * * Author: Nelio Software * Author URI: https://neliosoftware.com * License: GPL-2.0+ * License URI: http://www.gnu.org/licenses/gpl-2.0.txt * * Text Domain: nelio */ if ( ! defined( 'ABSPATH' ) ) { exit; }

Jeśli zrobiłeś to dobrze, zobaczysz, że możesz teraz aktywować wtyczkę w WordPress:

Zrzut ekranu naszej wtyczki z zabawkami na liście wtyczek
Zrzut ekranu z naszą zabawkową wtyczką na liście wtyczek.

Jasne, wtyczka jeszcze nic nie robi… ale przynajmniej się pojawia

Maszynopis

Dodajmy trochę kodu TypeScript! Pierwszą rzeczą, którą zrobimy, jest zainicjowanie npm w naszym folderze wtyczek. Uruchom to:

 npm init

i postępuj zgodnie z instrukcjami wyświetlanymi na ekranie. Następnie zainstaluj zależności:

 npm add -D @wordpress/scripts @wordpress/i18n

i edytuj plik package.json , aby dodać skrypty kompilacji wymagane przez @wordpress/scripts :

 { ... "scripts": { "build": "wp-scripts build", "start": "wp-scripts start", }, ... }

Gdy npm będzie gotowy, dostosujmy TypeScript, dodając plik tsconfig.json :

 { "compilerOptions": { "target": "es5", "module": "esnext", "moduleResolution": "node", "outDir": "build", "lib": [ "es7", "dom" ] }, "exclude": [ "node_modules" ] }

Na koniec napiszmy trochę kodu TS. Chcemy, aby było to bardzo proste, ale „wystarczająco zbliżone” do tego, co mieliśmy w Nelio A/B Testing i Nelio Content, więc utwórz folder src w naszym projekcie z kilkoma plikami TypeScript w środku: index.ts i utils/index.ts .

Z jednej strony załóżmy, że utils/index.ts to pakiet narzędzi. Oznacza to, że zawiera kilka funkcji, których mogą potrzebować inne pliki w naszym projekcie. Załóżmy na przykład, że zapewnia klasyczne funkcje min i max :

 export const min = ( a: number, b: number ): number => a < b ? a : b; export const max = ( a: number, b: number ): number => a > b ? a : b;

Z drugiej strony przyjrzyjmy się głównemu plikowi naszej aplikacji: index.ts . Do naszych celów testowych potrzebujemy tylko prostego skryptu, który korzysta z naszego pakietu narzędzi i zależności od WordPressa. Coś takiego:

 import { __, sprintf } from '@wordpress/i18n'; import { min } from './utils'; const a = 2; const b = 3; const m = min( a, b ); console.log( sprintf( /* translators: 1 -> num, 2 -> num, 3 -> num */ __( 'Min between %1$s and %2$s is %3$s', 'nelio' ), a, b, m ) );

@wordpress/skrypty Ustawienia domyślne

Gdybyśmy mieli teraz zbudować projekt za pomocą npm run build , wszystko działałoby po wyjęciu z pudełka. A to po prostu dlatego, że @wordpress/scripts (tj. podstawowe narzędzie, którego używamy do budowania naszego projektu) zostało zaprojektowane do pracy z bazą kodu taką jak w naszym przykładzie. Oznacza to, że jeśli mamy plik index.ts w folderze src , wygeneruje plik index.js w folderze build wraz z plikiem zależności index.asset.php :

 > ls build index.asset.php index.js

Dlaczego dwa pliki? Cóż, jeden to skompilowany plik JavaScript (duh), a drugi to plik zależności z przydatnymi informacjami o naszym skrypcie. W szczególności informuje nas, na których bibliotekach JavaScript, spośród tych zawartych w WordPressie, opiera się. Na przykład nasz index.ts opiera się na pakiecie @wordpress/i18n do internacjonalizacji ciągów, a jest to biblioteka zawarta w WordPress, więc… tak, wp-i18n pojawi się w index.asset.php :

 build/index.asset.php <?php return array( 'dependencies' => array( 'wp-i18n' ), 'version' => 'c6131c7f24df4fa803b7', );

Niestety domyślna konfiguracja nie jest idealna, jeśli mnie o to pytasz. Dlatego.

Jeśli wprowadzimy błąd w Twoim kodzie (np. wywołajmy funkcję min z argumentem string zamiast number ):

 const m = min( `${ a }`, b );

powinno to wywołać błąd. Ale tak nie jest. Kompiluje się bez problemów.

Sprawdzanie typu podczas kompilacji za pomocą TypeScript

Aby rozwiązać powyższe „ograniczenie”, wystarczy utworzyć własny plik konfiguracyjny pakietu webpack i powiedzieć mu, aby używał tsc (kompilatora TypeScript) za każdym razem, gdy napotka kod TS. Innymi słowy, potrzebujemy następującego pliku webpack.config.json :

 const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); const config = { ...defaultConfig, module: { ...defaultConfig.module, rules: [ ...defaultConfig.module.rules, { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, }; module.exports = { ...config, entry: './src/index', output: { path: __dirname + '/build', filename: 'index.js', }, };

Jak widać, zaczyna się od załadowania domyślnej konfiguracji webpacka zawartej w pakiecie @wordpress/scripts , a następnie rozszerza defaultConfig , dodając ts-loader do wszystkich plików .ts . Bułka z masłem!

A teraz oto:

 > npm run build ... ERROR in ...src/index.ts TS2345: Argument of type 'string' is not assignable to parameter of type 'number'. webpack 5.74.0 compiled with 1 error in 4470 ms

kompilacja naszego projektu kończy się błędem. Hurra! Jasne, jest trochę wolniejszy, ale przynajmniej mamy pewne kontrole bezpieczeństwa przed przesłaniem czegokolwiek do produkcji.

Kolejkowanie skryptów

Cóż, teraz, gdy wiesz, że w twoim kodzie jest problem, napraw go i ponownie skompiluj wtyczkę. Czy to wszystko działało? Chłodny! Ponieważ teraz nadszedł czas na kolejkowanie skryptu i jego zależności od PHP, abyśmy mogli go wypróbować w naszej przeglądarce.

Otwórz nelio.php i dołącz następujący fragment:

 add_action( 'admin_enqueue_scripts', function() { $path = untrailingslashit( plugin_dir_path( __FILE__ ) ); $url = untrailingslashit( plugin_dir_url( __FILE__ ) ); $asset = require( $path . '/build/index.asset.php' ); wp_enqueue_script( 'nelio', $url . '/build/index.js', $asset['dependencies'], $asset['version'] ); } );

Następnie przejdź do pulpitu WordPress (dowolna strona załatwi sprawę) i spójrz na konsolę JavaScript przeglądarki. Powinieneś zobaczyć następujący tekst:

 Min between 2 and 3 is 2

Miły!

A co z moimi zależnościami?

Porozmawiajmy przez chwilę o zarządzaniu zależnościami w JavaScript/webpack/WordPress. @wordpress/scripts jest skonfigurowany w taki sposób, że domyślnie, jeśli Twój projekt używa zależności spakowanej w rdzeniu WordPressa, zostanie on wymieniony jako taki w pliku .asset.php . To na przykład wyjaśnia, dlaczego @wordpress/i18n został wymieniony w pliku zależności naszego skryptu.

Ale co z zależnościami od „innych” pakietów? Co się stało z naszym utils ? Krótko mówiąc: domyślnie webpack kompiluje i łączy wszystkie zależności w skrypcie wyjściowym Wystarczy spojrzeć na wygenerowany plik JS (skompiluj go z npm run start , aby wyłączyć minifikację):

 ... var __webpack_modules__ = ({ "./src/utils/index.ts": ((...) => ... var min = function (a, b) { return a < b ? a : b; }; var max = function (a, b) { return a > b ? a : b; }; }), ...

Widzieć? utils kod narzędziowy jest właśnie tam, osadzony w naszym skrypcie wyjściowym.

A co z @wordpress/i18n ? Cóż, to tylko proste odniesienie do zmiennej globalnej:

 ... var __webpack_modules__ = ({ "./src/utils/index.ts": ..., "@wordpress/i18n": ((module)) => { module.exports = window["wp"]["i18n"]; }) ...

Jak już mówiłem, @wordpress/scripts zawiera wtyczkę, Dependency Extraction Webpack Plugin , która „wyklucza” pewne zależności z procesu kompilacji i generuje kod zakładając, że będą one dostępne w zasięgu globalnym. Na przykład w naszym przykładzie widzimy, że @wordpress/i18n znajduje się w wp.i18n . Dlatego podczas kolejkowania naszego skryptu musimy również kolejkować jego zależności.

Konfiguracja niestandardowa do generowania dwóch oddzielnych skryptów

Mając to wszystko na uwadze, powiedzmy, że chcemy osiągnąć to samo dzięki naszemu utils narzędzi. Oznacza to, że nie chcemy, aby jego zawartość była osadzona w index.js , ale raczej powinna być skompilowana do własnego pliku .js i pojawiać się jako zależność od index.asset.php . Jak to zrobimy?

Najpierw powinniśmy zmienić nazwę instrukcji import w index.js , aby wyglądała na prawdziwy pakiet. Innymi słowy, zamiast importować skrypt przy użyciu ścieżki względnej ( ./utils ), byłoby miło, gdybyśmy mogli użyć nazwy takiej jak @nelio/utils . Aby to zrobić, wystarczy edytować plik package.json projektu, aby dodać nową zależność w dependencies :

 { ... "dependencies": { "@nelio/utils": "./src/utils" }, "devDependencies": { "@wordpress/i18n": ..., "@wordpress/scripts": ... }, ... }

uruchom npm install , aby utworzyć dowiązanie symboliczne w node_modules wskazujące na ten „nowy” pakiet, a na koniec uruchom npm init w src/utils , aby z punktu widzenia npm @nelio/utils był poprawnym pakietem.

Następnie, aby skompilować @nelio/utils do jego własnego skryptu, musimy edytować naszą konfigurację webpack.config.js i zdefiniować dwa eksporty:

  • ten, który już mieliśmy ( ./src/index.ts )
  • kolejny eksport do kompilacji ./src/utils do innego pliku, eksponując jego eksporty w zmiennej globalnej o nazwie np. nelio.utils .

Innymi słowy, chcemy tego:

 module.exports = [ { ...config, entry: './src/index', output: { path: __dirname + '/build', filename: 'index.js', }, }, { ...config, entry: './src/utils', output: { path: __dirname + '/build', filename: 'utils.js', library: [ 'nelio', 'utils' ], libraryTarget: 'window', }, }, ];

Ponownie skompiluj kod i spójrz na folder ./build — zobaczysz, że wszyscy mamy teraz dwa skrypty. Spójrz na ./build/utils.js , a zobaczysz, jak definiuje nelio.utils , zgodnie z oczekiwaniami:

 ... var min = function (a, b) { return a < b ? a : b; }; var max = function (a, b) { return a > b ? a : b; }; (window.nelio = window.nelio || {}).utils = __webpack_exports__; ... ... var min = function (a, b) { return a < b ? a : b; }; var max = function (a, b) { return a > b ? a : b; }; (window.nelio = window.nelio || {}).utils = __webpack_exports__; ...

Niestety jesteśmy dopiero w połowie. Jeśli spojrzysz również na ./build/index.js , zobaczysz, że src/utils jest nadal w nim osadzone… czy nie powinno to być „zewnętrzną zależnością” i używać właśnie zdefiniowanej zmiennej globalnej?

Konfiguracja niestandardowa do tworzenia zależności zewnętrznych

Aby przekształcić @nelio/utils w rzeczywistą zależność zewnętrzną, musimy jeszcze bardziej dostosować nasz webpack i skorzystać z wtyczki do ekstrakcji zależności, o której wspominaliśmy wcześniej. Po prostu otwórz ponownie plik webpack.config.js i zmodyfikuj zmienną config w następujący sposób:

 const DependencyExtractionWebpackPlugin = require( '@wordpress/dependency-extraction-webpack-plugin' ); const config = { ...defaultConfig, module: { ...defaultConfig.module, rules: [ ...defaultConfig.module.rules, { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, plugins: [ ...defaultConfig.plugins.filter( ( p ) => p.constructor.name !== 'DependencyExtractionWebpackPlugin' ), new DependencyExtractionWebpackPlugin( { requestToExternal: ( request ) => '@nelio/utils' === request ? [ 'nelio', 'utils' ] : undefined, requestToHandle: ( request ) => '@nelio/utils' === request ? 'nelio-utils' : undefined, outputFormat: 'php', } ), ], };

tak, że wszystkie odniesienia do @nelio/utils są tłumaczone w kodzie jako nelio.utils i istnieje zależność od obsługi skryptu nelio-utils . Jeśli przyjrzymy się zależnościom obu skryptów, zobaczymy:

 build/index.asset.php <?php return array( 'dependencies' => array('nelio-utils', 'wp-i18n')... ?> build/utils.asset.php <?php return array( 'dependencies' => array()... ?>

a jeśli zajrzymy do ./build/index.js , potwierdzimy, że rzeczywiście @nelio/utils jest teraz zewnętrzna:

 ... var __webpack_modules__ = ({ "@nelio/utils": ((module)) => { module.exports = window["nelio"]["utils"]; }), "@wordpress/i18n": ((module)) => { module.exports = window["wp"]["i18n"]; }) ...

Jest jednak jeszcze jeden problem, którym musimy się zająć. Przejdź do przeglądarki, odśwież stronę dashboardu i spójrz na konsolę. Nic się nie pojawia, prawda? Czemu? Cóż, nelio teraz zależy od nelio-utils , ale ten skrypt nie jest zarejestrowany w WordPressie… więc jego zależności nie mogą być teraz spełnione. Aby to naprawić, edytuj nelio.php i zarejestruj nowy skrypt:

 add_action( 'admin_enqueue_scripts', function() { $path = untrailingslashit( plugin_dir_path( __FILE__ ) ); $url = untrailingslashit( plugin_dir_url( __FILE__ ) ); $asset = require( $path . '/build/utils.asset.php' ); wp_register_script( 'nelio-utils', $url . '/build/utils.js', $asset['dependencies'], $asset['version'] ); } );

Jak przyspieszyć proces budowania

Jeśli uruchomimy proces kompilacji kilka razy i uśrednimy czas potrzebny na ukończenie, zobaczymy, że kompilacja działa w około 10 sekund:

 > yarn run build ... ./src/index.ts + 2 modules ... webpack 5.74.0 compiled successfully in 5703 ms ... ./src/utils/index.ts ... webpack 5.74.0 compiled successfully in 5726 m Done in 10.22s.

co może wydawać się niewiele, ale jest to prosty projekt-zabawka i, jak już mówiłem, prawdziwe projekty, takie jak Nelio Content lub Nelio A/B Testing, potrzebują kilku minut na skompilowanie.

Dlaczego jest „tak wolno” i co możemy zrobić, aby to przyspieszyć? O ile mogłem powiedzieć, problem tkwił w konfiguracji naszego webpacka. Im więcej eksportów masz w module.exports twojego pakietu module.exports , tym wolniejszy staje się czas kompilacji. Jednak pojedynczy eksport jest o wiele szybszy.

Zrefaktoryzujmy nieco nasz projekt, aby użyć pojedynczego eksportu. Przede wszystkim utwórz plik export.ts w src/utils o następującej zawartości:

 export * as utils from './index';

Następnie edytuj webpack.config.js , aby zawierał pojedynczy eksport z dwoma wpisami:

 module.exports = { ...config, entry: { index: './src/index', utils: './src/utils/export', }, output: { path: __dirname + '/dist', filename: 'js/[name].js', library: { name: 'nelio', type: 'assign-properties', }, }, };

Na koniec skompiluj projekt ponownie:

 > yarn run build ... built modules 522 bytes [built] ./src/index.ts + 2 modules ... ./src/utils/export.ts + 1 modules ... webpack 5.74.0 compiled successfully in 4339 ms Done in 6.02s.

Zajęło to tylko 6 sekund, czyli prawie o połowę mniej niż kiedyś! Całkiem fajnie, co?

Streszczenie

TypeScript pomoże ci poprawić jakość kodu , ponieważ pozwala sprawdzić w czasie kompilacji, czy typy są poprawne i nie ma niespójności. Ale jak wszystko w życiu, zalety korzystania z TypeScript mają swoją cenę: kompilacja kodu staje się nieco wolniejsza.

W dzisiejszym poście widzieliśmy, że w zależności od konfiguracji twojego webpacka kompilacja twojego projektu może być znacznie szybsza (lub wolniejsza). Sweet spot wymaga jednego eksportu… io tym dzisiaj rozmawialiśmy.

Mam nadzieję, że podobał Ci się post. Jeśli tak, udostępnij to. Jeśli znasz inne sposoby na optymalizację webpacka, poinformuj mnie o tym w komentarzu poniżej. Miłego dnia!

Polecane zdjęcie autorstwa Saffu na Unsplash.