避免自動內聯代碼的陷阱
已發表: 2022-03-10 內聯是將文件內容直接包含在 HTML 文檔中的過程:CSS 文件可以內聯在style
元素中,而 JavaScript 文件可以內聯在script
元素中:
<style> /* CSS contents here */ </style> <script> /* JS contents here */ </script>
通過打印 HTML 輸出中已經存在的代碼,內聯避免了呈現阻塞請求並在呈現頁面之前執行代碼。 因此,它對於提高站點的感知性能(即頁面變為可用所需的時間)很有用。例如,我們可以使用加載站點時立即傳遞的數據緩衝區(大約 14kb)內聯關鍵樣式,包括首屏內容的樣式(就像在之前的 Smashing Magazine 網站上所做的那樣),以及字體大小和佈局寬度和高度,以避免在傳遞其餘數據時重新呈現跳躍的佈局.
但是,當過度內聯代碼也會對站點的性能產生負面影響:因為代碼不可緩存,相同的內容會重複發送到客戶端,並且無法通過 Service Worker 進行預緩存,或者從內容交付網絡緩存和訪問。 此外,在實施內容安全策略 (CSP) 時,內聯腳本被認為是不安全的。 然後,它制定了一個明智的策略來內聯 CSS 和 JS 的那些關鍵部分,這些部分使網站加載速度更快,但盡可能避免。
為了避免內聯,在本文中,我們將探討如何將內聯代碼轉換為靜態資源:我們不將代碼打印在 HTML 輸出中,而是將其保存到磁盤(有效地創建一個靜態文件)並添加相應的<script>
或<link>
標籤來加載文件。
讓我們開始吧!
推薦閱讀: WordPress 安全作為一個進程
何時避免內聯
沒有什麼神奇的秘訣可以確定某些代碼是否必須內聯,但是,當某些代碼不能內聯時,這一點非常明顯:當它涉及大量代碼時,以及何時不需要立即內聯。
例如,WordPress 站點內聯 JavaScript 模板以呈現媒體管理器(可在/wp-admin/upload.php
下的媒體庫頁面中訪問),打印大量代碼:
佔用整整 43kb,這段代碼的大小是不可忽略的,因為它位於頁面底部,所以不需要立即使用。 因此,通過靜態資產提供此代碼或將其打印在 HTML 輸出中會很有意義。
接下來讓我們看看如何將內聯代碼轉換為靜態資產。
觸發創建靜態文件
如果內容(要內聯的內容)來自靜態文件,那麼除了簡單地請求該靜態文件而不是內聯代碼之外,沒有什麼可做的。
但是,對於動態代碼,我們必須計劃如何/何時生成包含其內容的靜態文件。 例如,如果站點提供配置選項(例如更改配色方案或背景圖像),那麼應該何時生成包含新值的文件? 我們有以下機會從動態代碼創建靜態文件:
- 根據要求
當用戶第一次訪問內容時。 - 改變時
當動態代碼的來源(例如配置值)發生變化時。
讓我們先根據要求考慮。 用戶第一次訪問該站點時,比如說通過/index.html
,靜態文件(例如header-colors.css
)還不存在,因此必須在那時生成它。 事件順序如下:
- 用戶請求
/index.html
; - 在處理請求時,服務器會檢查文件
header-colors.css
存在。 既然沒有,就獲取源代碼並在磁盤上生成文件; - 它向客戶端返迴響應,包括標籤
<link rel="stylesheet" type="text/css" href="/staticfiles/header-colors.css">
- 瀏覽器獲取頁面中包含的所有資源,包括
header-colors.css
; - 到那時這個文件已經存在,所以它被提供了。
但是,事件的順序也可能不同,導致結果不理想。 例如:
- 用戶請求
/index.html
; - 該文件已經被瀏覽器(或其他代理,或通過 Service Worker)緩存,因此請求永遠不會發送到服務器;
- 瀏覽器獲取頁面中包含的所有資源,包括
header-colors.css
。 然而,這個圖像並沒有緩存在瀏覽器中,所以請求被發送到服務器; - 服務器尚未生成
header-colors.css
(例如,它剛剛重新啟動); - 它將返回 404。
或者,我們可以不在請求/index.html
時生成header-colors.css
,而是在請求/header-colors.css
本身時生成。 但是,由於該文件最初不存在,因此該請求已被視為 404。即使我們可以繞過它,更改標頭以將狀態代碼更改為 200,並返回圖像的內容,這是一種糟糕的做事方式,所以我們不會考慮這種可能性(我們比這要好得多!)
只剩下一個選擇:在源更改後生成靜態文件。
源更改時創建靜態文件
請注意,我們可以從依賴於用戶和依賴於站點的源創建動態代碼。 例如,如果主題允許更改站點的背景圖像並且該選項由站點管理員配置,則可以生成靜態文件作為部署過程的一部分。 另一方面,如果站點允許其用戶更改其配置文件的背景圖像,則必須在運行時生成靜態文件。
簡而言之,我們有以下兩種情況:
- 用戶配置
當用戶更新配置時,必須觸發該過程。 - 站點配置
當管理員更新站點的配置時或部署站點之前,必須觸發該過程。
如果我們獨立考慮這兩種情況,對於#2,我們可以在我們想要的任何技術堆棧上設計流程。 但是,我們不想實現兩種不同的解決方案,而是一種可以解決這兩種情況的獨特解決方案。 並且因為從#1 開始,生成靜態文件的過程必須在運行的站點上觸發,因此圍繞站點運行的相同技術堆棧設計這個過程是很有說服力的。
在設計流程時,我們的代碼需要處理#1和#2的具體情況:
- 版本控制
必須使用“版本”參數訪問靜態文件,以便在創建新靜態文件時使先前的文件無效。 雖然#2 可以簡單地與站點具有相同的版本控制,但#1 需要為每個用戶使用動態版本,可能保存在數據庫中。 - 生成文件的位置
#2 為整個站點生成一個唯一的靜態文件(例如/staticfiles/header-colors.css
),而 #1 為每個用戶創建一個靜態文件(例如/staticfiles/users/leo/header-colors.css
)。 - 觸發事件
對於#1,靜態文件必須在運行時執行,而對於#2,它也可以在我們的暫存環境中作為構建過程的一部分執行。 - 部署和分發
#2 中的靜態文件可以無縫集成到站點的部署包中,沒有任何挑戰; 但是,#1 中的靜態文件不能,因此該過程必須處理其他問題,例如負載均衡器後面的多個服務器(靜態文件是僅在 1 個服務器中創建,還是在所有服務器中創建,以及如何創建?)。
接下來讓我們設計和實現該流程。 對於要生成的每個靜態文件,我們必須創建一個包含文件元數據的對象,從動態源計算其內容,最後將靜態文件保存到磁盤。 作為指導以下解釋的用例,我們將生成以下靜態文件:
-
header-colors.css
,具有數據庫中保存的值的一些樣式 welcomeuser-data.js
,包含一個 JSON 對象,其中包含某個變量下的用戶數據:window.welcomeUserData = {name: "Leo"};
.
下面,我將描述為 WordPress 生成靜態文件的過程,我們必須將堆棧基於 PHP 和 WordPress 函數。 部署前生成靜態文件的功能可以通過加載執行短代碼[create_static_files]
的特殊頁面來觸發,正如我在上一篇文章中描述的那樣。
進一步推薦閱讀:製作服務工作者:案例研究
將文件表示為對象
我們必須將文件建模為具有所有相應屬性的 PHP 對象,因此我們既可以將文件保存在磁盤上的特定位置(例如,在/staticfiles/
或/staticfiles/users/leo/
下),並且知道如何請求因此歸檔。 為此,我們創建了一個接口Resource
,返回文件的元數據(文件名、目錄、類型:“css”或“js”、版本以及對其他資源的依賴項)及其內容。
interface Resource { function get_filename(); function get_dir(); function get_type(); function get_version(); function get_dependencies(); function get_content(); }
為了使代碼可維護和可重用,我們遵循 SOLID 原則,為此我們為資源設置了一個對象繼承方案,以逐步添加屬性,從抽像類ResourceBase
開始,我們所有的 Resource 實現都將從該抽像類 ResourceBase 開始:
abstract class ResourceBase implements Resource { function get_dependencies() { // By default, a file has no dependencies return array(); } }
遵循 SOLID,只要屬性不同,我們就會創建子類。 如前所述,生成的靜態文件的位置以及請求它的版本控制將根據與用戶或站點配置有關的文件而有所不同:
abstract class UserResourceBase extends ResourceBase { function get_dir() { // A different file and folder for each user $user = wp_get_current_user(); return "/staticfiles/users/{$user->user_login}/"; } function get_version() { // Save the resource version for the user under her meta data. // When the file is regenerated, must execute `update_user_meta` to increase the version number $user_id = get_current_user_id(); $meta_key = "resource_version_".$this->get_filename(); return get_user_meta($user_id, $meta_key, true); } } abstract class SiteResourceBase extends ResourceBase { function get_dir() { // All files are placed in the same folder return "/staticfiles/"; } function get_version() { // Same versioning as the site, assumed defined under a constant return SITE_VERSION; } }
最後,在最後一層,我們為要生成的文件實現對象,通過函數get_content
添加文件名、文件類型和動態代碼:
class HeaderColorsSiteResource extends SiteResourceBase { function get_filename() { return "header-colors"; } function get_type() { return "css"; } function get_content() { return sprintf( " .site-title a { color: #%s; } ", esc_attr(get_header_textcolor()) ); } } class WelcomeUserDataUserResource extends UserResourceBase { function get_filename() { return "welcomeuser-data"; } function get_type() { return "js"; } function get_content() { $user = wp_get_current_user(); return sprintf( "window.welcomeUserData = %s;", json_encode( array( "name" => $user->display_name ) ) ); } }
有了這個,我們將文件建模為 PHP 對象。 接下來,我們需要將其保存到磁盤。
將靜態文件保存到磁盤
將文件保存到磁盤可以通過語言提供的本機功能輕鬆完成。 對於 PHP,這是通過函數fwrite
完成的。 此外,我們創建了一個實用程序類ResourceUtils
,其函數提供磁盤上文件的絕對路徑,以及相對於站點根目錄的路徑:
class ResourceUtils { protected static function get_file_relative_path($fileObject) { return $fileObject->get_dir().$fileObject->get_filename().".".$fileObject->get_type(); } static function get_file_path($fileObject) { // Notice that we must add constant WP_CONTENT_DIR to make the path absolute when saving the file return WP_CONTENT_DIR.self::get_file_relative_path($fileObject); } } class ResourceGenerator { static function save($fileObject) { $file_path = ResourceUtils::get_file_path($fileObject); $handle = fopen($file_path, "wb"); $numbytes = fwrite($handle, $fileObject->get_content()); fclose($handle); } }
然後,每當源更改並且需要重新生成靜態文件時,我們執行ResourceGenerator::save
並將表示文件的對像作為參數傳遞。 下面的代碼重新生成文件“header-colors.css”和“welcomeuser-data.js”並將其保存在磁盤上:
// When need to regenerate header-colors.css, execute: ResourceGenerator::save(new HeaderColorsSiteResource()); // When need to regenerate welcomeuser-data.js, execute: ResourceGenerator::save(new WelcomeUserDataUserResource());
一旦它們存在,我們就可以通過<script>
和<link>
標籤將要加載的文件排入隊列。
將靜態文件排隊
將靜態文件排入隊列與將 WordPress 中的任何資源排入隊列沒有什麼不同:通過函數wp_enqueue_script
和wp_enqueue_style
。 然後,我們簡單地迭代所有對象實例並根據它們的get_type()
值是"js"
還是"css"
使用一個或另一個鉤子。
我們首先添加實用函數來提供文件的 URL,並告訴類型是 JS 還是 CSS:
class ResourceUtils { // Continued from above... static function get_file_url($fileObject) { // Add the site URL before the file path return get_site_url().self::get_file_relative_path($fileObject); } static function is_css($fileObject) { return $fileObject->get_type() == "css"; } static function is_js($fileObject) { return $fileObject->get_type() == "js"; } }
ResourceEnqueuer
類的實例將包含所有必須加載的文件; 調用時,其函數enqueue_scripts
和enqueue_styles
將通過執行相應的 WordPress 函數(分別為wp_enqueue_script
和wp_enqueue_style
)來進行排隊:
class ResourceEnqueuer { protected $fileObjects; function __construct($fileObjects) { $this->fileObjects = $fileObjects; } protected function get_file_properties($fileObject) { $handle = $fileObject->get_filename(); $url = ResourceUtils::get_file_url($fileObject); $dependencies = $fileObject->get_dependencies(); $version = $fileObject->get_version(); return array($handle, $url, $dependencies, $version); } function enqueue_scripts() { $jsFileObjects = array_map(array(ResourceUtils::class, 'is_js'), $this->fileObjects); foreach ($jsFileObjects as $fileObject) { list($handle, $url, $dependencies, $version) = $this->get_file_properties($fileObject); wp_register_script($handle, $url, $dependencies, $version); wp_enqueue_script($handle); } } function enqueue_styles() { $cssFileObjects = array_map(array(ResourceUtils::class, 'is_css'), $this->fileObjects); foreach ($cssFileObjects as $fileObject) { list($handle, $url, $dependencies, $version) = $this->get_file_properties($fileObject); wp_register_style($handle, $url, $dependencies, $version); wp_enqueue_style($handle); } } }
最後,我們用代表每個文件的 PHP 對象列表實例化ResourceEnqueuer
類的對象,並添加一個 WordPress 掛鉤來執行入隊:
// Initialize with the corresponding object instances for each file to enqueue $fileEnqueuer = new ResourceEnqueuer( array( new HeaderColorsSiteResource(), new WelcomeUserDataUserResource() ) ); // Add the WordPress hooks to enqueue the resources add_action('wp_enqueue_scripts', array($fileEnqueuer, 'enqueue_scripts')); add_action('wp_print_styles', array($fileEnqueuer, 'enqueue_styles'));
就是這樣:在排隊時,將在客戶端加載站點時請求靜態文件。 我們成功地避免了打印內聯代碼和加載靜態資源。
接下來,我們可以應用一些改進來獲得額外的性能提升。
推薦閱讀:使用 PHPUnit 自動測試 WordPress 插件的介紹
將文件捆綁在一起
儘管 HTTP/2 減少了捆綁文件的需求,但它仍然使站點更快,因為文件壓縮(例如通過 GZip)會更有效,並且因為瀏覽器(例如 Chrome)處理許多資源的開銷更大.
到目前為止,我們已經將文件建模為 PHP 對象,這允許我們將此對象視為其他進程的輸入。 特別是,我們可以重複上述相同的過程,將同一類型的所有文件捆綁在一起,並提供捆綁版本而不是所有獨立文件。 為此,我們創建了一個函數get_content
,它簡單地從$fileObjects
下的每個資源中提取內容,然後再次打印它,生成來自所有資源的所有內容的聚合:
abstract class SiteBundleBase extends SiteResourceBase { protected $fileObjects; function __construct($fileObjects) { $this->fileObjects = $fileObjects; } function get_content() { $content = ""; foreach ($this->fileObjects as $fileObject) { $content .= $fileObject->get_content().PHP_EOL; } return $content; } }
我們可以通過為這個文件創建一個類來將所有文件捆綁到文件bundled-styles.css
:
class StylesSiteBundle extends SiteBundleBase { function get_filename() { return "bundled-styles"; } function get_type() { return "css"; } }
最後,我們只是像以前一樣將這些捆綁的文件排入隊列,而不是所有獨立的資源。 對於 CSS,我們創建一個包含文件header-colors.css
、 background-image.css
和font-sizes.css
,為此我們只需使用 PHP 對象為這些文件中的每一個實例化StylesSiteBundle
(同樣我們可以創建 JS捆綁文件):
$fileObjects = array( // CSS new HeaderColorsSiteResource(), new BackgroundImageSiteResource(), new FontSizesSiteResource(), // JS new WelcomeUserDataUserResource(), new UserShoppingItemsUserResource() ); $cssFileObjects = array_map(array(ResourceUtils::class, 'is_css'), $fileObjects); $jsFileObjects = array_map(array(ResourceUtils::class, 'is_js'), $fileObjects); // Use this definition of $fileEnqueuer instead of the previous one $fileEnqueuer = new ResourceEnqueuer( array( new StylesSiteBundle($cssFileObjects), new ScriptsSiteBundle($jsFileObjects) ) );
而已。 現在我們將隻請求一個 JS 文件和一個 CSS 文件,而不是很多。
感知性能的最終改進涉及通過延遲加載那些不需要立即使用的資產來確定資產的優先級。 讓我們接下來解決這個問題。
JS 資源的async
/ defer
屬性
我們可以將屬性async
和defer
添加到<script>
標籤,以改變 JavaScript 文件何時被下載、解析和執行,以優先考慮關鍵 JavaScript 並儘可能晚地推送所有非關鍵內容,從而減少網站的明顯加載時間。
為了實現這個特性,遵循 SOLID 原則,我們應該創建一個包含函數is_async
和is_defer
的新接口JSResource
(它繼承自Resource
)。 然而,這將關閉<style>
標籤最終也支持這些屬性。 因此,考慮到適應性,我們採取了一種更開放的方法:我們只需將通用方法get_attributes
添加到接口Resource
以保持靈活地添加到任何屬性(已經存在或尚未發明的<script>
) <script>
和<link>
標籤:
interface Resource { // Continued from above... function get_attributes(); } abstract class ResourceBase implements Resource { // Continued from above... function get_attributes() { // By default, no extra attributes return ''; } }
WordPress 沒有提供一種簡單的方法來為入隊的資源添加額外的屬性,所以我們以一種相當老套的方式來做,添加一個鉤子,通過函數add_script_tag_attributes
替換標籤內的字符串:
class ResourceEnqueuerUtils { protected static tag_attributes = array(); static function add_tag_attributes($handle, $attributes) { self::tag_attributes[$handle] = $attributes; } static function add_script_tag_attributes($tag, $handle, $src) { if ($attributes = self::tag_attributes[$handle]) { $tag = str_replace( " src='${src}'>", " src='${src}' ".$attributes.">", $tag ); } return $tag; } } // Initize by connecting to the WordPress hook add_filter( 'script_loader_tag', array(ResourceEnqueuerUtils::class, 'add_script_tag_attributes'), PHP_INT_MAX, 3 );
我們在創建相應的對象實例時為資源添加屬性:
abstract class ResourceBase implements Resource { // Continued from above... function __construct() { ResourceEnqueuerUtils::add_tag_attributes($this->get_filename(), $this->get_attributes()); } }
最後,如果資源welcomeuser-data.js
不需要立即執行,我們可以將其設置為defer
:
class WelcomeUserDataUserResource extends UserResourceBase { // Continued from above... function get_attributes() { return "defer='defer'"; } }
因為它是作為延遲加載的,所以腳本將稍後加載,從而提前用戶可以與站點交互的時間點。 關於性能提升,我們現在都準備好了!
在我們放鬆之前,還有一個問題需要解決:當網站託管在多台服務器上時會發生什麼?
處理負載均衡器後面的多個服務器
如果我們的站點託管在負載均衡器後面的多個站點上,並且重新生成了與用戶配置相關的文件,則處理請求的服務器必須以某種方式將重新生成的靜態文件上傳到所有其他服務器; 否則,從那一刻起,其他服務器將提供該文件的陳舊版本。 我們如何做到這一點? 讓服務器相互通信不僅複雜,而且最終可能被證明是不可行的:如果站點運行在來自不同地區的數百台服務器上會發生什麼? 顯然,這不是一個選擇。
我想出的解決方案是添加一個間接級別:不是從站點 URL 請求靜態文件,而是從雲中的某個位置(例如從 AWS S3 存儲桶)請求它們。 然後,在重新生成文件後,服務器會立即將新文件上傳到 S3 並從那裡提供服務。 這個解決方案的實現在我之前的文章通過 AWS S3 在多個服務器之間共享數據中進行了解釋。
結論
在本文中,我們認為內聯 JS 和 CSS 代碼並不總是理想的,因為代碼必須重複發送到客戶端,如果代碼量很大,這可能會影響性能。 例如,我們看到 WordPress 如何加載 43kb 的腳本來打印媒體管理器,這些腳本是純 JavaScript 模板,可以完美地作為靜態資源加載。
因此,我們設計了一種通過將動態 JS 和 CSS 內聯代碼轉換為靜態資源來提高網站速度的方法,這可以增強多個級別的緩存(在客戶端、Service Workers、CDN),允許進一步將所有文件捆綁在一起只放入一個 JS/CSS 資源中,以提高壓縮輸出時的比率(例如通過 GZip)並避免瀏覽器同時處理多個資源(例如在 Chrome 中)的開銷,此外還允許添加屬性async
或defer
到<script>
標籤以加快用戶交互性,從而改善網站的明顯加載時間。
作為一個有益的副作用,將代碼拆分為靜態資源還可以讓代碼更清晰,處理代碼單元而不是 HTML 的大塊,這可以更好地維護項目。
我們開發的解決方案是用 PHP 完成的,包括一些特定的 WordPress 代碼,但是,代碼本身非常簡單,幾乎沒有幾個定義屬性的接口和遵循 SOLID 原則實現這些屬性的對象,以及一個保存文件到磁盤。 差不多就是這樣。 最終結果是簡潔緊湊,可以直接為任何其他語言和平台重新創建,並且不難引入現有項目 - 提供輕鬆的性能提升。