Menambahkan Kemampuan Pemisahan Kode ke Situs WordPress Melalui PoP
Diterbitkan: 2022-03-10Kecepatan adalah salah satu prioritas utama untuk situs web mana pun saat ini. Salah satu cara untuk membuat situs web memuat lebih cepat adalah dengan pemecahan kode: membagi aplikasi menjadi beberapa bagian yang dapat dimuat sesuai permintaan — memuat hanya JavaScript yang diperlukan yang diperlukan dan tidak ada yang lain. Situs web berdasarkan kerangka kerja JavaScript dapat segera menerapkan pemecahan kode melalui Webpack, bundel JavaScript yang populer. Namun, untuk situs web WordPress, itu tidak mudah. Pertama, Webpack tidak sengaja dibuat untuk bekerja dengan WordPress, jadi pengaturannya akan memerlukan beberapa solusi; kedua, tampaknya tidak ada alat yang menyediakan kemampuan pemuatan aset sesuai permintaan asli untuk WordPress.
Mengingat kurangnya solusi yang tepat untuk WordPress, saya memutuskan untuk mengimplementasikan versi saya sendiri dari pemecahan kode untuk PoP, kerangka kerja sumber terbuka untuk membangun situs web WordPress yang saya buat. Situs web WordPress dengan PoP yang diinstal akan memiliki kemampuan pemecahan kode secara asli, sehingga tidak perlu bergantung pada Webpack atau bundler lainnya. Dalam artikel ini, saya akan menunjukkan cara melakukannya, menjelaskan keputusan apa yang diambil berdasarkan aspek arsitektur kerangka kerja. Pada akhirnya, saya akan menganalisis kinerja situs web dengan dan tanpa pemecahan kode, serta keuntungan dan kerugian menggunakan implementasi khusus di atas bundel eksternal. Saya harap Anda menikmati perjalanannya!
Mendefinisikan Strategi
Pemisahan kode secara garis besar dapat dibagi menjadi dua langkah ini:
- Menghitung aset mana yang harus dimuat untuk setiap rute,
- Memuat aset tersebut secara dinamis sesuai permintaan.
Untuk mengatasi langkah pertama, kita perlu membuat peta dependensi aset, termasuk semua aset dalam aplikasi kita. Aset harus ditambahkan secara rekursif ke peta ini — dependensi dari dependensi juga harus ditambahkan, hingga tidak ada lagi aset yang diperlukan. Kami kemudian dapat menghitung semua dependensi yang diperlukan untuk rute tertentu dengan melintasi peta dependensi aset, mulai dari titik masuk rute (yaitu file atau potongan kode dari mana ia memulai eksekusi) sampai ke tingkat terakhir.
Untuk mengatasi langkah kedua, kita dapat menghitung aset mana yang diperlukan untuk URL yang diminta di sisi server, dan kemudian mengirim daftar aset yang dibutuhkan sebagai respons, yang diperlukan aplikasi untuk memuatnya, atau langsung HTTP/ 2 dorong sumber daya di samping respons.
Solusi ini, bagaimanapun, tidak optimal. Dalam kasus pertama, aplikasi harus meminta semua aset setelah respons dikembalikan, sehingga akan ada serangkaian permintaan bolak-balik tambahan untuk mengambil aset, dan tampilan tidak dapat dibuat sebelum semuanya dimuat, mengakibatkan pengguna harus menunggu (masalah ini diredakan dengan membuat semua aset di-cache melalui service worker, sehingga waktu tunggu berkurang, tetapi kita tidak dapat menghindari penguraian aset yang terjadi hanya setelah respons kembali). Dalam kasus kedua, kami mungkin mendorong aset yang sama berulang kali (kecuali kami menambahkan beberapa logika tambahan, seperti untuk menunjukkan sumber daya mana yang telah kami muat melalui cookie, tetapi ini memang menambah kompleksitas yang tidak diinginkan dan memblokir respons agar tidak di-cache), dan kami tidak dapat melayani aset dari CDN.
Karena itu, saya memutuskan untuk menangani logika ini di sisi klien. Daftar aset mana yang diperlukan untuk setiap rute tersedia untuk aplikasi pada klien, sehingga aplikasi tersebut sudah mengetahui aset mana yang diperlukan untuk URL yang diminta. Ini mengatasi masalah yang disebutkan di atas:
- Aset dapat segera dimuat, tidak harus menunggu respons server. (Saat kita memasangkannya dengan service worker, kita bisa yakin bahwa, pada saat respons kembali, semua sumber daya telah dimuat dan diuraikan, jadi tidak ada waktu tunggu tambahan.)
- Aplikasi mengetahui aset mana yang telah dimuat; karenanya, tidak akan meminta semua aset yang diperlukan untuk rute itu, tetapi hanya aset yang belum dimuat.
Aspek negatif untuk mengirimkan daftar ini ke ujung depan adalah mungkin menjadi berat, tergantung pada ukuran situs web (seperti berapa banyak rute yang tersedia). Kita perlu menemukan cara untuk memuatnya tanpa menambah waktu pemuatan yang dirasakan aplikasi. Lebih lanjut tentang ini nanti.
Setelah membuat keputusan ini, kita dapat melanjutkan ke desain dan kemudian mengimplementasikan pemecahan kode dalam aplikasi. Untuk memudahkan pemahaman, proses telah dibagi menjadi langkah-langkah berikut:
- Memahami arsitektur aplikasi,
- Memetakan dependensi aset,
- Daftar semua rute aplikasi,
- Menghasilkan daftar yang mendefinisikan aset mana yang diperlukan untuk setiap rute,
- Memuat aset secara dinamis,
- Menerapkan pengoptimalan.
Mari kita langsung ke dalamnya!
0. Memahami Arsitektur Aplikasi
Kita perlu memetakan hubungan semua aset satu sama lain. Mari kita membahas kekhasan arsitektur PoP untuk merancang solusi yang paling sesuai untuk mencapai tujuan ini.
PoP adalah lapisan yang membungkus WordPress, memungkinkan kita untuk menggunakan WordPress sebagai CMS yang menggerakkan aplikasi, namun menyediakan kerangka kerja JavaScript khusus untuk merender konten di sisi klien untuk membangun situs web dinamis. Ini mendefinisikan kembali komponen bangunan halaman web: sedangkan WordPress saat ini didasarkan pada konsep templat hierarkis yang menghasilkan HTML (seperti single.php
, home.php
dan archive.php
), PoP didasarkan pada konsep "modul, ” yang merupakan fungsionalitas atomik atau komposisi modul lain. Membangun aplikasi PoP mirip dengan bermain dengan LEGO — menumpuk modul di atas satu sama lain atau membungkus satu sama lain, yang pada akhirnya menciptakan struktur yang lebih kompleks. Itu juga bisa dianggap sebagai implementasi dari desain atom Brad Frost, dan terlihat seperti ini:
Modul dapat dikelompokkan ke dalam entitas tingkat tinggi, yaitu: blok, grup blok, bagian halaman, dan Level atas. Entitas ini juga merupakan modul, hanya dengan properti dan tanggung jawab tambahan, dan mereka berisi satu sama lain mengikuti arsitektur top-down yang ketat di mana setiap modul dapat melihat dan mengubah properti dari semua modul dalamnya. Hubungan antar modul seperti ini:
- 1 topLevel berisi N halamanBagian,
- 1 halamanBagian berisi N blok atau grup blok,
- 1 blockGroup berisi N blok atau blockGroups,
- 1 blok berisi N modul,
- 1 modul berisi N modul, tak terhingga.
Menjalankan Kode JavaScript di PoP
PoP secara dinamis membuat HTML dengan, mulai dari level pageSection, mengulangi semua modul di baris berikutnya, merender masing-masing modul melalui template Handlebars yang telah ditentukan sebelumnya dan, terakhir, menambahkan elemen modul yang baru dibuat terkait ke dalam DOM. Setelah ini selesai, ia menjalankan fungsi JavaScript pada mereka, yang telah ditentukan sebelumnya berdasarkan modul demi modul.
PoP berbeda dari kerangka kerja JavaScript (seperti React dan AngularJS) dalam hal aliran aplikasi tidak berasal dari klien, tetapi masih dikonfigurasi di bagian belakang, di dalam konfigurasi modul (yang dikodekan dalam objek PHP). Dipengaruhi oleh kait tindakan WordPress, PoP mengimplementasikan pola terbitkan–berlangganan:
- Setiap modul mendefinisikan fungsi JavaScript mana yang harus dijalankan pada elemen DOM yang baru dibuat terkait, tidak perlu mengetahui sebelumnya apa yang akan mengeksekusi kode ini atau dari mana asalnya.
- Objek JavaScript harus mendaftarkan fungsi JavaScript mana yang mereka implementasikan.
- Terakhir, pada saat runtime, PoP menghitung objek JavaScript mana yang harus menjalankan fungsi JavaScript mana, dan memanggilnya dengan tepat.
Misalnya, melalui objek PHP yang sesuai, modul kalender menunjukkan bahwa ia memerlukan fungsi calendar
untuk dieksekusi pada elemen DOM-nya seperti ini:
class CalendarModule { function get_jsmethods() { $methods = parent::get_jsmethods(); $this->add_jsmethod($methods, 'calendar'); return $methods; } ... }
Kemudian, objek JavaScript — dalam hal ini, popFullCalendar
— mengumumkan bahwa itu mengimplementasikan fungsi calendar
. Ini dilakukan dengan memanggil popJSLibraryManager.register
:
window.popFullCalendar = { calendar : function(elements) { ... } }; popJSLibraryManager.register(popFullCalendar, ['calendar', ...]);
Akhirnya, popJSLibraryManager
melakukan pencocokan pada apa yang mengeksekusi kode apa. Ini memungkinkan objek JavaScript untuk mendaftarkan fungsi mana yang mereka implementasikan, dan menyediakan metode untuk menjalankan fungsi tertentu dari semua objek JavaScript yang berlangganan:
window.popJSLibraryManager = { libraries: [], methods: {}, register : function(library, methods) { this.libraries.push(library); for (var i = 0; i < methods.length; i++) { var method = methods[i]; this.methods[method] = this.methods[method] || []; this.methods[method].push(library); } }, execute : function(method, elements) { var libraries = this.methods[method] || []; for (var i = 0; i < libraries.length; i++) { var library = libraries[i]; library[method](elements); } } }
Setelah elemen kalender baru ditambahkan ke DOM, yang memiliki ID calendar-293
, PoP hanya akan menjalankan fungsi berikut:
popJSLibraryManager.execute("calendar", document.getElementById("calendar-293"));
Titik masuk
Untuk PoP, titik masuk untuk mengeksekusi kode JavaScript adalah baris ini di akhir keluaran HTML:
<script type="text/javascript">popManager.init();</script>
popManager.init()
pertama-tama menginisialisasi kerangka kerja front-end, dan kemudian menjalankan fungsi JavaScript yang ditentukan oleh semua modul yang dirender, seperti yang dijelaskan di atas. Di bawah ini adalah bentuk yang sangat sederhana dari fungsi ini (kode aslinya ada di GitHub). Dengan menjalankan popJSLibraryManager.execute('pageSectionInitialized', pageSection)
dan popJSLibraryManager.execute('documentInitialized')
, semua objek JavaScript yang mengimplementasikan fungsi tersebut ( pageSectionInitialized
dan documentInitialized
) akan menjalankannya.
(function($){ window.popManager = { // The configuration for all the modules (including pageSections and blocks) in the application configuration : {...}, init : function() { var that = this; $.each(this.configuration, function(pageSectionId, configuration) { // Obtain the pageSection element in the DOM from the ID var pageSection = $('#'+pageSectionId); // Run all required JavaScript methods on it this.runJSMethods(pageSection, configuration); // Trigger an event marking the block as initialized popJSLibraryManager.execute('pageSectionInitialized', pageSection); }); // Trigger an event marking the document as initialized popJSLibraryManager.execute('documentInitialized'); }, ... }; })(jQuery);
Fungsi runJSMethods
mengeksekusi metode JavaScript yang ditentukan untuk setiap modul, mulai dari pageSection, yang merupakan modul paling atas, dan kemudian ke bawah untuk semua blok bagian dalam dan modul bagian dalamnya:
(function($){ window.popManager = { ... runJSMethods : function(pageSection, configuration) { // Initialize the heap with "modules", starting from the top one, and recursively iterate over its inner modules var heap = [pageSection.data('module')], i; while (heap.length > 0) { // Get the first element of the heap var module = heap.pop(); // The configuration for that module contains which JavaScript methods to execute, and which are the module's inner modules var moduleConfiguration = configuration[module]; // The list of all JavaScript functions that must be executed on the module's newly created DOM elements var jsMethods = moduleConfiguration['js-methods']; // Get all of the elements added to the DOM for that module, which have been stored in JavaScript object `popJSRuntimeManager` upon creation var elements = popJSRuntimeManager.getDOMElements(module); // Iterate through all of the JavaScript methods and execute them, passing the elements as argument for (i = 0; i < jsMethods.length; i++) { popJSLibraryManager.execute(jsMethods[i], elements); } // Finally, add the inner-modules to the heap heap = heap.concat(moduleConfiguration['inner-modules']); } }, }; })(jQuery);
Singkatnya, eksekusi JavaScript di PoP digabungkan secara longgar: Alih-alih memiliki dependensi yang diperbaiki, kami menjalankan fungsi JavaScript melalui kait yang dapat dilanggani oleh objek JavaScript apa pun.
Halaman Web dan API
Situs web PoP adalah API yang dikonsumsi sendiri. Di PoP, tidak ada perbedaan antara halaman web dan API: Setiap URL mengembalikan halaman web secara default, dan hanya dengan menambahkan parameter output=json
, ia mengembalikan API-nya (misalnya, getpop.org/en/ adalah halaman web, dan getpop.org/en/?output=json adalah API-nya). API digunakan untuk merender konten secara dinamis dalam PoP; jadi, ketika mengklik tautan ke halaman lain, API-lah yang diminta, karena pada saat itu kerangka situs web akan dimuat (seperti navigasi atas dan samping) — maka kumpulan sumber daya yang dibutuhkan untuk mode API akan menjadi bagian dari itu dari halaman web. Kita perlu mempertimbangkan hal ini saat menghitung dependensi untuk suatu rute: Memuat rute saat pertama kali memuat situs web atau memuatnya secara dinamis dengan mengklik beberapa tautan akan menghasilkan kumpulan aset yang diperlukan yang berbeda.
Ini adalah aspek terpenting dari PoP yang akan menentukan desain dan implementasi pemecahan kode. Mari kita lanjutkan dengan langkah berikutnya.
1. Memetakan Ketergantungan Aset
Kita dapat menambahkan file konfigurasi untuk setiap file JavaScript, merinci dependensi eksplisitnya. Namun, ini akan menduplikasi kode dan akan sulit untuk tetap konsisten. Solusi yang lebih bersih adalah dengan menyimpan file JavaScript sebagai satu-satunya sumber kebenaran, mengekstrak kode dari dalamnya dan kemudian menganalisis kode ini untuk membuat ulang dependensi.
Meta data yang kami cari di file sumber JavaScript, untuk dapat membuat ulang pemetaan, adalah sebagai berikut:
- panggilan metode internal, seperti
this.runJSMethods(...)
; - panggilan metode eksternal, seperti
popJSRuntimeManager.getDOMElements(...)
; - semua kemunculan
popJSLibraryManager.execute(...)
, yang mengeksekusi fungsi JavaScript di semua objek yang mengimplementasikannya; - semua kemunculan
popJSLibraryManager.register(...)
, untuk mendapatkan objek JavaScript mana yang mengimplementasikan metode JavaScript mana.
Kami akan menggunakan jParser dan jTokenizer untuk menandai file sumber JavaScript kami di PHP dan mengekstrak data meta, sebagai berikut:
- Panggilan metode internal (seperti
this.runJSMethods
) disimpulkan ketika menemukan urutan berikut: baik tokenthis
atauthat
+.
+ beberapa token lain, yang merupakan nama untuk metode internal (runJSMethods
). - Panggilan metode eksternal (seperti
popJSRuntimeManager.getDOMElements
) disimpulkan ketika menemukan urutan berikut: token yang termasuk dalam daftar semua objek JavaScript di aplikasi kita (kita akan memerlukan daftar ini terlebih dahulu; dalam hal ini, ini akan berisi objekpopJSRuntimeManager
) +.
+ beberapa token lain, yang merupakan nama untuk metode eksternal (getDOMElements
). - Setiap kali kami menemukan
popJSLibraryManager.execute("someFunctionName")
kami menyimpulkan metode Javascript menjadisomeFunctionName
. - Setiap kali kami menemukan
popJSLibraryManager.register(someJSObject, ["someFunctionName1", "someFunctionName2"])
kami menyimpulkan objek JavascriptsomeJSObject
untuk mengimplementasikan metodesomeFunctionName1
,someFunctionName2
.
Saya telah menerapkan skrip tetapi tidak akan menjelaskannya di sini. (Terlalu panjang tidak menambah banyak nilai, tetapi dapat ditemukan di repositori PoP). Skrip, yang berjalan saat meminta halaman internal di server pengembangan situs web (metodologi yang telah saya tulis di artikel sebelumnya tentang pekerja layanan), akan menghasilkan file pemetaan dan menyimpannya di server. Saya telah menyiapkan contoh file pemetaan yang dihasilkan. Ini adalah file JSON sederhana, yang berisi atribut berikut:
-
internalMethodCalls
Untuk setiap objek JavaScript, buat daftar dependensi dari fungsi internal di antara mereka sendiri. -
externalMethodCalls
Untuk setiap objek JavaScript, buat daftar dependensi dari fungsi internal ke fungsi dari objek JavaScript lainnya. -
publicMethods
Buat daftar semua metode terdaftar dan, untuk setiap metode, objek JavaScript mana yang mengimplementasikannya. -
methodExecutions
Untuk setiap objek JavaScript dan setiap fungsi internal, buat daftar semua metode yang dijalankan melaluipopJSLibraryManager.execute('someMethodName')
.
Harap dicatat bahwa hasilnya bukan peta dependensi aset, melainkan peta dependensi objek JavaScript. Dari peta ini, kita dapat menetapkan, setiap kali suatu fungsi dari beberapa objek dieksekusi, objek lain apa yang juga akan diperlukan. Kita masih perlu mengonfigurasi objek JavaScript mana yang terdapat di setiap aset, untuk semua aset (dalam skrip jTokenizer, objek JavaScript adalah token yang kita cari untuk mengidentifikasi pemanggilan metode eksternal, jadi informasi ini merupakan input ke skrip dan dapat 't diperoleh dari file sumber itu sendiri). Ini dilakukan melalui objek PHP ResourceLoaderProcessor
, seperti resourceloader-processor.php.
Terakhir, dengan menggabungkan peta dan konfigurasi, kita akan dapat menghitung semua aset yang diperlukan untuk setiap rute dalam aplikasi.
2. Daftar Semua Rute Aplikasi
Kita perlu mengidentifikasi semua rute yang tersedia di aplikasi kita. Untuk situs web WordPress, daftar ini akan dimulai dengan URL dari setiap hierarki template. Yang diimplementasikan untuk PoP adalah sebagai berikut:
- halaman rumah: https://getpop.org/en/
- penulis: https://getpop.org/en/u/leo/
- tunggal: https://getpop.org/en/blog/new-feature-code-splitting/
- tag: https://getpop.org/en/tags/internet/
- halaman: https://getpop.org/en/philosophy/
- kategori: https://getpop.org/en/blog/ (kategori sebenarnya diimplementasikan sebagai halaman, untuk menghapus
category/
dari jalur URL) - 404: https://getpop.org/en/this-page-does-not-exist/
Untuk masing-masing hierarki ini, kita harus mendapatkan semua rute yang menghasilkan konfigurasi unik (yaitu yang akan membutuhkan satu set aset unik). Dalam kasus PoP, kami memiliki yang berikut:
- halaman rumah dan 404 unik.
- Laman tag selalu memiliki konfigurasi yang sama untuk tag apa pun. Dengan demikian, satu URL untuk tag apa pun sudah cukup.
- Posting tunggal bergantung pada kombinasi jenis postingan (seperti “acara” atau “posting”) dan kategori utama postingan (seperti “blog” atau “artikel”). Kemudian, kita memerlukan URL untuk setiap kombinasi ini.
- Konfigurasi halaman kategori tergantung pada kategori. Jadi, kita membutuhkan URL dari setiap kategori posting.
- Halaman penulis tergantung pada peran penulis ("individu", "organisasi" atau "komunitas"). Jadi, kita memerlukan URL untuk tiga penulis, masing-masing dengan salah satu peran ini.
- Setiap halaman dapat memiliki konfigurasinya sendiri ("login", "hubungi kami", "misi kami", dll.). Jadi, semua URL halaman harus ditambahkan ke daftar.
Seperti yang bisa kita lihat, daftarnya sudah cukup panjang. Selain itu, aplikasi kita dapat menambahkan parameter ke URL yang mengubah konfigurasi, yang berpotensi juga mengubah aset mana yang diperlukan. PoP, misalnya, menawarkan untuk menambahkan parameter URL berikut:
- tab (
?tab=…
), untuk menampilkan informasi terkait: https://getpop.org/en/blog/new-feature-code-splitting/?tab=authors; - format (
?format=…
), untuk mengubah tampilan data: https://getpop.org/en/blog/?format=list; - target (
?target=…
), untuk membuka halaman di halaman yang berbedaBagian: https://getpop.org/en/add-post/?target=addons.
Beberapa rute awal dapat memiliki satu, dua atau bahkan tiga parameter di atas, menciptakan beragam kombinasi:
- satu posting: https://getpop.org/en/blog/new-feature-code-splitting/
- penulis posting tunggal: https://getpop.org/en/blog/new-feature-code-splitting/?tab=authors
- penulis posting tunggal sebagai daftar: https://getpop.org/en/blog/new-feature-code-splitting/?tab=authors&format=list
- penulis posting tunggal sebagai daftar di jendela modal: https://getpop.org/en/blog/new-feature-code-splitting/?tab=authors&format=list&target=modals
Singkatnya, untuk PoP, semua rute yang mungkin adalah kombinasi dari item berikut:
- semua rute hierarki template awal;
- semua nilai berbeda yang hierarkinya akan menghasilkan konfigurasi berbeda;
- semua kemungkinan tab untuk setiap hierarki (hierarki yang berbeda mungkin memiliki nilai tab yang berbeda: Satu postingan dapat memiliki tab “penulis” dan “tanggapan”, sementara seorang penulis dapat memiliki tab “postingan” dan “pengikut”);
- semua format yang mungkin untuk setiap tab (tab yang berbeda dapat menerapkan format yang berbeda: tab "penulis" mungkin memiliki format "peta", tetapi tab "tanggapan" mungkin tidak);
- semua kemungkinan target yang menunjukkan halamanBagian di mana setiap rute dapat ditampilkan (sementara posting dapat dibuat di bagian utama atau di jendela mengambang, halaman "Berbagi dengan teman Anda" dapat diatur untuk dibuka di jendela modal).
Oleh karena itu, untuk aplikasi yang sedikit rumit, membuat daftar dengan semua rute tidak dapat dilakukan secara manual. Kita harus, kemudian, membuat skrip untuk mengekstrak informasi ini dari database, memanipulasinya dan, akhirnya, mengeluarkannya dalam format yang diperlukan. Script ini akan mendapatkan semua kategori posting, dari mana kami dapat menghasilkan daftar semua URL halaman kategori yang berbeda, dan kemudian, untuk setiap kategori, kueri database untuk setiap posting di bawah yang sama, yang akan menghasilkan URL untuk satu posting di bawah setiap kategori, dan seterusnya. Skrip lengkap tersedia, mulai dari function get_resources()
, yang memaparkan kait untuk diimplementasikan oleh setiap kasus hierarki.
3. Menghasilkan Daftar Yang Mendefinisikan Aset Yang Diperlukan Untuk Setiap Rute
Sekarang, kami memiliki peta dependensi aset dan daftar semua rute dalam aplikasi. Sekarang saatnya untuk menggabungkan keduanya dan menghasilkan daftar yang menunjukkan, untuk setiap rute, aset mana yang diperlukan.
Untuk membuat daftar ini, kami menerapkan prosedur berikut:
- Hasilkan daftar yang berisi semua metode JavaScript yang akan dieksekusi untuk setiap rute:
Hitung modul rute, lalu dapatkan konfigurasi untuk setiap modul, lalu ekstrak dari konfigurasi fungsi JavaScript yang perlu dijalankan modul, dan tambahkan semuanya bersama-sama. - Selanjutnya, telusuri peta dependensi aset untuk setiap fungsi JavaScript, kumpulkan daftar semua dependensi yang diperlukan, dan tambahkan semuanya bersama-sama.
- Terakhir, tambahkan template Handlebars yang diperlukan untuk merender setiap modul di dalam rute itu.
Selain itu, seperti yang dinyatakan sebelumnya, setiap URL memiliki halaman web dan mode API, jadi kita perlu menjalankan prosedur di atas dua kali, sekali untuk setiap mode (yaitu sekali menambahkan parameter output=json
ke URL, yang mewakili rute untuk mode API, dan sekali menjaga URL tidak berubah untuk mode halaman web). Kami kemudian akan menghasilkan dua daftar, yang akan memiliki kegunaan yang berbeda:
- Daftar mode halaman web akan digunakan saat pertama kali memuat situs web, sehingga skrip yang sesuai untuk rute tersebut disertakan dalam respons HTML awal. Daftar ini akan disimpan di server.
- Daftar mode API akan digunakan saat memuat halaman secara dinamis di situs web. Daftar ini akan dimuat pada klien, untuk memungkinkan aplikasi menghitung aset tambahan apa yang harus dimuat, sesuai permintaan, saat tautan diklik.
Sebagian besar logika telah diimplementasikan mulai dari function add_resources_from_settingsprocessors($fetching_json, ...)
, (Anda dapat menemukannya di repositori). Parameter $fetching_json
membedakan antara mode halaman web ( false
) dan API ( true
).
Saat skrip untuk mode halaman web dijalankan, skrip akan menampilkan resourceloader-bundle-mapping.json, yang merupakan objek JSON dengan properti berikut:
-
bundle-ids
Ini adalah kumpulan hingga empat sumber daya (namanya telah diubah untuk lingkungan produksi:eq
=>handlebars
,er
=>handlebars-helpers
, dll.), dikelompokkan di bawah bundel ID. -
bundlegroup-ids
Ini adalah kumpulanbundle-ids
. Setiap bundleGroup mewakili satu set sumber daya yang unik. -
key-ids
Ini adalah pemetaan antara rute (diwakili oleh hash mereka, yang mengidentifikasi kumpulan semua atribut yang membuat rute unik) dan bundleGroup yang sesuai.
Seperti yang dapat diamati, pemetaan antara rute dan sumber dayanya tidak lurus. Alih-alih memetakan key-ids
ke daftar sumber daya, ini memetakannya ke bundleGroup unik, yang merupakan daftar bundles
, dan hanya setiap bundel yang merupakan daftar resources
(hingga empat elemen setiap bundel). Mengapa dilakukan seperti ini? Ini melayani dua tujuan:
- Ini memungkinkan kami untuk mengidentifikasi semua sumber daya di bawah bundleGroup yang unik. Jadi, alih-alih menyertakan semua sumber daya dalam respons HTML, kami dapat menyertakan aset JavaScript unik, yang merupakan file bundleGroup yang sesuai, yang digabungkan dalam semua sumber daya yang sesuai. Ini berguna saat melayani perangkat yang masih tidak mendukung HTTP/2, dan juga akan meningkatkan waktu pemuatan, karena meng-zip file paket tunggal lebih efektif daripada mengompresi file penyusunnya sendiri dan kemudian menambahkannya bersama-sama. Atau, kami juga dapat memuat serangkaian bundel alih-alih bundleGroup unik, yang merupakan kompromi antara sumber daya dan bundleGroups (memuat bundel lebih lambat daripada bundleGroups karena Gzip'ing, tetapi lebih berkinerja jika pembatalan sering terjadi, sehingga kami hanya akan mengunduh bundel yang diperbarui dan bukan seluruh bundleGroup). Skrip untuk menggabungkan semua sumber daya ke dalam bundel dan bundleGroups ditemukan di filegenerator-bundles.php dan filegenerator-bundlegroups.php.
- Membagi kumpulan sumber daya ke dalam bundel memungkinkan kita untuk mengidentifikasi pola umum (misalnya, mengidentifikasi kumpulan empat sumber daya yang dibagi di antara banyak rute), akibatnya memungkinkan rute yang berbeda untuk terhubung ke bundel yang sama. Akibatnya, daftar yang dihasilkan akan memiliki ukuran yang lebih kecil. Ini mungkin tidak berguna untuk daftar halaman web, yang hidup di server, tetapi bagus untuk daftar API, yang akan dimuat di klien, seperti yang akan kita lihat nanti.
Saat skrip untuk mode API dijalankan, skrip akan menampilkan file resources.js, dengan properti berikut:
-
bundles
danbundle-groups
melayani tujuan yang sama seperti yang dinyatakan untuk mode halaman web -
keys
juga melayani tujuan yang sama sepertikey-ids
untuk mode halaman web. Namun, alih-alih memiliki hash sebagai kunci untuk mewakili rute, ini adalah gabungan dari semua atribut yang membuat rute unik — dalam kasus kami, format (f
), tab (t
) dan target (r
). -
sources
adalah file sumber untuk setiap sumber daya. -
types
adalah CSS atau JavaScript untuk setiap sumber daya (meskipun, demi kesederhanaan, kami belum membahas dalam artikel ini bahwa sumber daya JavaScript juga dapat mengatur sumber daya CSS sebagai dependensi, dan modul dapat memuat aset CSS mereka sendiri, menerapkan strategi pemuatan CSS progresif ). -
resources
menangkap bundleGroups mana yang harus dimuat untuk setiap hierarki. -
ordered-load-resources
berisi sumber daya mana yang harus dimuat secara berurutan, untuk mencegah skrip dimuat sebelum skrip dependennya (secara default, skrip tersebut asinkron).
Kami akan mengeksplorasi cara menggunakan file ini di bagian selanjutnya.
4. Memuat Aset Secara Dinamis
Seperti yang dinyatakan, daftar API akan dimuat di klien, sehingga kami dapat mulai memuat aset yang diperlukan untuk rute segera setelah pengguna mengklik tautan.
Memuat Skrip Pemetaan
File JavaScript yang dihasilkan dengan daftar sumber daya untuk semua rute dalam aplikasi tidak ringan — dalam hal ini, menjadi 85 KB (yang sendiri dioptimalkan, setelah merusak nama sumber daya dan menghasilkan bundel untuk mengidentifikasi pola umum di seluruh rute) . Waktu penguraian seharusnya tidak menjadi hambatan besar, karena penguraian JSON 10 kali lebih cepat daripada penguraian JavaScript untuk data yang sama. Namun, ukurannya adalah masalah dari transfer jaringan, jadi kami harus memuat skrip ini dengan cara yang tidak memengaruhi waktu pemuatan aplikasi yang dirasakan atau membuat pengguna menunggu.
Solusi yang saya terapkan adalah melakukan precache file ini menggunakan service worker, memuatnya menggunakan defer
sehingga tidak memblokir utas utama saat menjalankan metode JavaScript penting, dan kemudian menampilkan pesan pemberitahuan mundur jika pengguna mengklik tautan sebelum skrip dimuat: "Situs web masih memuat, harap tunggu beberapa saat untuk mengklik tautan." Ini dilakukan dengan menambahkan div tetap dengan kelas layar loadingscreen
yang ditempatkan di atas segalanya saat skrip dimuat, lalu menambahkan pesan notifikasi, dengan kelas notificationmsg
, di dalam div, dan beberapa baris CSS berikut:
.loadingscreen > .notificationmsg { display: none; } .loadingscreen:focus > .notificationmsg, .loadingscreen:active > .notificationmsg { display: block; }
Solusi lain adalah dengan membagi file ini menjadi beberapa file dan memuatnya secara bertahap sesuai kebutuhan (strategi yang sudah saya kodekan). Selain itu, file 85 KB mencakup semua rute yang mungkin dalam aplikasi, termasuk rute seperti "pengumuman penulis, ditampilkan dalam thumbnail, ditampilkan di jendela modals," yang mungkin diakses sekali di bulan biru, jika sama sekali. Rute yang paling banyak diakses hanya sedikit (halaman rumah, tunggal, penulis, tag dan semua halaman, semuanya tanpa atribut tambahan), yang seharusnya menghasilkan file yang jauh lebih kecil, sekitar 30 KB.
Mendapatkan Rute Dari URL yang Diminta
Kami harus dapat mengidentifikasi rute dari URL yang diminta. Contohnya:
-
https://getpop.org/en/u/leo/
memetakan ke rute "penulis", -
https://getpop.org/en/u/leo/?tab=followers
memetakan ke rute "pengikut penulis", -
https://getpop.org/en/tags/internet/
memetakan ke "tag" rute, -
https://getpop.org/en/tags/
memetakan ke "halaman/tags/
" rute, - dan seterusnya.
Untuk mencapai ini, kita perlu mengevaluasi URL, dan menyimpulkan darinya elemen yang membuat rute unik: hierarki dan semua atribut (format, tab, dan target). Mengidentifikasi atribut tidak masalah, karena itu adalah parameter di URL. Satu-satunya tantangan adalah menyimpulkan hierarki (beranda, penulis, tunggal, halaman, atau tag) dari URL, dengan mencocokkan URL dengan beberapa pola. Sebagai contoh,
- Apa pun yang dimulai dengan
https://getpop.org/en/u/
adalah seorang penulis. - Apa pun yang dimulai dengan tetapi tidak persis
https://getpop.org/en/tags/
adalah sebuah tag. Jika tepatnyahttps://getpop.org/en/tags/
, maka itu adalah halaman. - Dan seterusnya.
Fungsi di bawah ini, yang diimplementasikan mulai dari baris 321 resourceloader.js, harus diisi dengan konfigurasi dengan pola untuk semua hierarki ini. Ini pertama-tama memeriksa apakah tidak ada subjalur di URL — dalam hal ini, ini adalah "rumah". Kemudian, ia memeriksa satu per satu untuk mencocokkan hierarki untuk "penulis", "tag" dan "tunggal". Jika tidak berhasil dengan salah satu dari itu, maka itu adalah kasus default, yaitu "halaman":
window.popResourceLoader = { // The config will be populated externally, using a config.js file, generated by a script config : {}, getPath : function(url) { var parser = document.createElement('a'); parser.href = url; return parser.pathname; }, getHierarchy : function(url) { var path = this.getPath(url); if (!path) { return 'home'; } var config = this.config; if (path.startsWith(config.paths.author) && path != config.paths.author) { return 'author'; } if (path.startsWith(config.paths.tag) && path != config.paths.tag) { return 'tag'; } // We must also check that this path is, itself, not a potential page (https://getpop.org/en/posts/articles/ is "page", but https://getpop.org/en/posts/this-is-a-post/ is "single") if (config.paths.single.indexOf(path) === -1 && config.paths.single.some(function(single_path) { return path.startsWith(single_path) && path != single_path;})) { return 'single'; } return 'page'; }, ... };
Karena semua data yang diperlukan sudah ada di database (semua kategori, semua slug halaman, dll.), kami akan mengeksekusi skrip untuk membuat file konfigurasi ini secara otomatis di lingkungan pengembangan atau staging. The implemented script is resourceloader-config.php, which produces config.js with the URL patterns for the hierarchies “author”, “tag” and “single”, under the key “paths”:
popResourceLoader.config = { "paths": { "author": "u/", "tag": "tags/", "single": ["posts/articles/", "posts/announcements/", ...] }, ... };
Loading Resources for the Route
Once we have identified the route, we can obtain the required assets from the generated JavaScript file under the key “resources”, which looks like this:
config.resources = { "home": { "1": [1, 110, ...], "2": [2, 111, ...], ... }, "author": { "7": [6, 114, ...], "8": [7, 114, ...], ... }, "tag": { "119": [66, 127, ...], "120": [66, 127, ...], ... }, "single": { "posts/": { "7": [190, 142, ...], "3": [190, 142, ...], ... }, "events/": { "7": [213, 389, ...], "3": [213, 389, ...], ... }, ... }, "page": { "log-in/": { "3": [233, 115, ...] }, "log-out/": { "3": [234, 115, ...] }, "add-post/": { "3": [239, 398, ...] }, "posts/": { "120": [268, 127, ...], "122": [268, 127, ...], ... }, ... } };
At the first level, we have the hierarchy (home, author, tag, single or page). Hierarchies are divided into two groups: those that have only one set of resources (home, author and tag), and those that have a specific subpath (page permalink for the pages, custom post type or category for the single). Finally, at the last level, for each key ID (which represents a unique combination of the possible values of “format”, “tab” and “target”, stored under “keys”), we have an array of two elements: [JS bundleGroup ID, CSS bundleGroup ID], plus additional bundleGroup IDs if executing progressive booting (JS bundleGroups to be loaded as "async" or "defer" are bundled separately; this will be explained in the optimizations section below).
Please note: For the single
hierarchy, we have different configurations depending on the custom post type. This can be reflected in the subpath indicated above (for example, events
and posts
) because this information is in the URL (for example, https://getpop.org/en/posts/the-winners-of-climate-change-techno-fixes/
and https://getpop.org/en/events/debate-post-fork/
), so that, when clicking on a link, we will know the corresponding post type and can thus infer the corresponding route. However, this is not the case with the author
hierarchy. As indicated earlier, an author may have three different configurations, depending on the user role ( individual
, organization
or community
); however, in this file, we've defined only one configuration for the author hierarchy, not three. That is because we are not able to tell from the URL what is the role of the author: user leo
(under https://getpop.org/en/u/leo/
) is an individual, whereas user pop
(under https://getpop.org/en/u/pop/
) is a community; however, their URLs have the same pattern. If we could instead have the URLs https://getpop.org/en/u/individuals/leo/
and https://getpop.org/en/u/communities/pop/
, then we could add a configuration for each user role. However, I've found no way to achieve this in WordPress. As a consequence, only for the API mode, we must merge the three routes (individuals, organizations and communities) into one, which will have all of the resources for the three cases; and clicking on the link for user leo
will also load the resources for organizations and communities, even if we don't need them.
Finally, when a URL is requested, we obtain its route, from which we obtain the bundleGroup IDs (for both JavaScript and CSS assets). From each bundleGroup, we find the corresponding bundles under bundlegroups
. Then, for each bundle, we obtain all resources under the key bundles
. Finally, we identify which assets have not yet been loaded, and we load them by getting their source, which is stored under the key sources
. The whole logic is coded starting from line 472 in resourceloader.js.
And with that, we have implemented code-splitting for our application! From now on, we can get better loading times by applying optimizations. Let's tackle that next.
5. Applying Optimizations
The objective is to load as little code as possible, as delayed as possible, and to cache as much of it as possible. Let's explore how to do this.
Splitting Up the Code Into Smaller Units
A single JavaScript asset may implement several functions (by calling popJSLibraryManager.register
), yet maybe only one of those functions is actually needed by the route. Thus, it makes sense to split up the asset into several subassets, implementing a single function on each of them, and extracting all common code from all of the functions into yet another asset, depended upon by all of them.
For instance, in the past, there was a unique file, waypoints.js
, that implemented the functions waypointsFetchMore
, waypointsTheater
and a few more. However, in most cases, only the function waypointsFetchMore
was needed, so I was loading the code for the function waypointsTheater
unnecessarily. Then, I split up waypoints.js
into the following assets:
- waypoints.js, with all common code and implementing no public functions;
- waypoints-fetchmore.js, which implements just the public function
waypointsFetchMore
; - waypoints-theater.js, which implements just the public function
waypointsTheater
.
Evaluating how to split the files is a manual job. Luckily, there is a tool that greatly eases the task: Chrome Developer Tools' “Coverage” tab, which displays in red those portions of JavaScript code that have not been invoked:
By using this tool, we can better understand how to split our JavaScript files into more granular units, thus reducing the amount of unneeded code that is loaded.
Integration With Service Workers
By precaching all of the resources using service workers, we can be pretty sure that, by the time the response is back from the server, all of the required assets will have been loaded and parsed. I wrote an article on Smashing Magazine on how to accomplish this.
Progressive Booting
PoP's architecture plays very nice with the concept of loading assets in different stages. When defining the JavaScript methods to execute on each module (by doing $this->add_jsmethod($methods, 'calendar')
), these can be set as either critical
or non-critical
. By default, all methods are set as non-critical, and critical methods must be explicitly defined by the developer, by adding an extra parameter: $this->add_jsmethod($methods, 'calendar', 'critical')
. Then, we will be able to load scripts immediately for critical functions, and wait until the page is loaded to load non-critical functions, the JavaScript files of which are loaded using defer
.
(function($){ window.popManager = { init : function() { var that = this; $.each(this.configuration, function(pageSectionId, configuration) { ... this.runJSMethods(pageSection, configuration, 'critical'); ... }); window.addEventListener('load', function() { $.each(this.configuration, function(pageSectionId, configuration) { ... this.runJSMethods(pageSection, configuration, 'non-critical'); ... }); }); ... }, ... }; })(jQuery);
The gains from progressive booting are major: The JavaScript engine needs not spend time parsing non-critical JavaScript initially, when a quick response to the user is most important, and overall reduces the time to interactive.
Testing And Analizying Performance Gains
We can use https://getpop.org/en/, a PoP website, for testing purposes. When loading the home page, opening Chrome Developer Tools' “Elements” tab and searching for “defer”, it shows 4 occurrences. Thanks to progressive booting, that is 4 bundleGroup JavaScript files containing the contents of 57 Javascript files with non-critical methods that could wait until the website finished loading to be loaded:
If we now switch to the “Network” tab and click on a link, we can see which assets get loaded. For instance, click on the link “Application/UX Features” on the left side. Filtering by JavaScript, we see it loaded 38 files, including JavaScript libraries and Handlebars templates. Filtering by CSS, we see it loaded 9 files. These 47 files have all been loaded on demand:
Let's check whether the loading time got boosted. We can use WebPagetest to measure the application with and without code-splitting, and calculate the difference.
- Without code-splitting: testing URL, WebPagetest results
- With code-splitting, loading resources: testing URL, WebPagetest Results
- With code-splitting, loading a bundleGroup: testing URL, WebPagetest Results
We can see that when loading the app bundle with all resources or when doing code-splitting and loading resources, there is not so much gain. However, when doing code-splitting and loading a bundleGroup, the gains are significant: 1.7 seconds in loading time, 500 milliseconds to the first meaningful paint, and 1 second to interactive.
Conclusion: Is It Worth It?
You might be thinking, Is it worth it all this trouble? Let's analyze the advantages and disadvantages of implementing our own code-splitting features.
Kekurangan
- Kita harus menjaganya.
Jika kami baru saja menggunakan Webpack, kami dapat mengandalkan komunitasnya untuk terus memperbarui perangkat lunak dan dapat mengambil manfaat dari ekosistem pluginnya. - Script membutuhkan waktu untuk dijalankan.
Situs web PoP Agenda Urbana memiliki 304 rute berbeda, dari mana ia menghasilkan 422 set sumber daya unik. Untuk situs web ini, menjalankan skrip yang menghasilkan peta dependensi aset, menggunakan MacBook Pro dari 2012, membutuhkan waktu sekitar 8 menit, dan menjalankan skrip yang menghasilkan daftar dengan semua sumber daya dan membuat file bundel dan bundleGroup membutuhkan waktu 15 menit . Itu lebih dari cukup waktu untuk minum kopi! - Ini membutuhkan lingkungan pementasan.
Jika kita perlu menunggu sekitar 25 menit untuk menjalankan skrip, maka kita tidak dapat menjalankannya di produksi. Kita perlu memiliki lingkungan pementasan dengan konfigurasi yang persis sama dengan sistem produksi. - Kode tambahan ditambahkan ke situs web, hanya untuk manajemen.
Kode 85 KB tidak berfungsi dengan sendirinya, tetapi hanya kode untuk mengelola kode lain. - Kompleksitas ditambahkan.
Hal ini tidak dapat dihindari dalam hal apapun jika kita ingin membagi aset kita menjadi unit yang lebih kecil. Webpack juga akan menambah kerumitan pada aplikasi.
Keuntungan
- Ini bekerja dengan WordPress.
Webpack tidak bekerja dengan WordPress di luar kotak, dan untuk membuatnya berfungsi perlu beberapa solusi. Solusi ini bekerja di luar kotak untuk WordPress (selama PoP diinstal). - Ini scalable dan extensible.
Ukuran dan kerumitan aplikasi dapat berkembang tanpa batas, karena file JavaScript dimuat sesuai permintaan. - Ini mendukung Gutenberg (alias WordPress masa depan).
Karena memungkinkan kita untuk memuat kerangka kerja JavaScript sesuai permintaan, ini akan mendukung blok Gutenberg (disebut Gutenblocks), yang diharapkan dikodekan dalam kerangka kerja yang dipilih oleh pengembang, dengan potensi hasil kerangka kerja yang berbeda yang dibutuhkan untuk aplikasi yang sama. - Ini nyaman.
Alat build menangani pembuatan file konfigurasi. Selain menunggu, tidak diperlukan usaha ekstra dari kami. - Itu membuat pengoptimalan menjadi mudah.
Saat ini, jika plugin WordPress ingin memuat aset JavaScript secara selektif, plugin tersebut akan menggunakan banyak persyaratan untuk memeriksa apakah ID halaman sudah benar. Dengan alat ini, tidak perlu untuk itu; prosesnya otomatis. - Aplikasi akan memuat lebih cepat.
Ini adalah seluruh alasan mengapa kami mengkodekan alat ini. - Ini membutuhkan lingkungan pementasan.
Efek samping positif adalah peningkatan keandalan: Kami tidak akan menjalankan skrip pada produksi, jadi kami tidak akan merusak apa pun di sana; proses penyebaran tidak akan gagal dari perilaku yang tidak terduga; dan pengembang akan dipaksa untuk menguji aplikasi menggunakan konfigurasi yang sama seperti di produksi. - Itu disesuaikan dengan aplikasi kita.
Tidak ada overhead atau solusi. Apa yang kami dapatkan persis seperti yang kami butuhkan, berdasarkan arsitektur yang kami kerjakan.
Kesimpulannya: ya, itu sangat berharga, karena sekarang kami dapat menerapkan aset beban sesuai permintaan di situs web WordPress kami dan membuatnya memuat lebih cepat.
Sumber Daya Lebih Lanjut
- Webpack, termasuk panduan ""Pemisahan Kode"
- “Pembuatan Webpack yang Lebih Baik” (video), K. Adam White
Integrasi Webpack dengan WordPress - “Gutenberg dan WordPress Masa Depan,” Morten Rand-Hendriksen, WP Tavern
- “WordPress Menjelajahi Pendekatan Kerangka-Agnostik JavaScript untuk Membangun Blok Gutenberg,” Sarah Gooding, WP Tavern