首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 合併 JS/CSS 請求:減少 HTTP 連接數提升頁面加載性能

     編輯:PHP 綜合知識

背景:HTTP/1.1 時代的連接開銷

雖然 HTTP/2 多路復用緩解了并發連接壓力,但許多環境仍運行在 HTTP/1.1 或需要兼容舊瀏覽器。合併靜態資源請求 (Combo) 能顯著減少:
🔗 TCP 握手/SSL 握手 次數
📦 HTTP Header 開銷 (每請求 ~500-800 bytes)
⏱ 隊首阻塞 等待時間
📊 典型頁面 10+ JS/CSS → 合併為 1-2 請求,首屏加載提速 30%-50%

核心原理

前端引用:  <script src="/combo.php?f=jquery,utils,app.js"></script>
               │
               ▼
PHP 接收 f 參數 → 讀取文件內容 → 拼接輸出 → 設置緩存頭 → 瀏覽器執行

生產級 Combo 服務實現

<?php
/**
 * 靜態資源合併服務 (支援 JS/CSS)
 * 特性:路徑安全、內容類型自動識別、ETag/Last-Modified、Gzip、錯誤容錯
 */
class ComboService {
    private $rootDir;
    private $maxFiles = 50;          // 單次最大合併數
    private $maxSize  = 5 * 1024 * 1024; // 5MB 總大小限制

    public function __construct(string $staticRoot) {
        $this->rootDir = rtrim(realpath($staticRoot), DIRECTORY_SEPARATOR);
    }

    public function handle(): void {
        $filesParam = $_GET['f'] ?? '';
        if (!$filesParam) {
            $this->error(400, 'Missing "f" parameter');
        }

        $fileList = $this->parseFileList($filesParam);
        if (empty($fileList)) {
            $this->error(400, 'No valid files');
        }

        // 確定輸出類型 (以第一個文件為準)
        $firstExt = pathinfo($fileList[0], PATHINFO_EXTENSION);
        $contentType = $this->getContentType($firstExt);
        header("Content-Type: $contentType; charset=UTF-8");

        // 緩存策略
        $this->setCacheHeaders($fileList);

        // 輸出合併內容
        $output = '';
        $totalSize = 0;
        foreach ($fileList as $file) {
            $fullPath = $this->resolvePath($file);
            if (!$fullPath || !is_readable($fullPath)) {
                error_log("Combo missing file: $file");
                continue; // 靜默跳過,避免單個缺失導致整頁崩潰
            }
            $content = file_get_contents($fullPath);
            $totalSize += strlen($content);
            if ($totalSize > $this->maxSize) {
                error_log("Combo size limit exceeded");
                break;
            }
            $output .= $content . "\n"; // 換行隔離,防止無分號合併報錯
        }

        // 支持 Gzip/Deflate
        if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== false) {
            header('Content-Encoding: gzip');
            $output = gzencode($output, 6);
        }

        echo $output;
    }

    /** 解析並去重文件列表 */
    private function parseFileList(string $param): array {
        $files = array_unique(array_map('trim', explode(',', $param)));
        return array_slice(array_filter($files, fn($f) => $f !== ''), 0, $this->maxFiles);
    }

    /** 路徑解析與安全檢查 (防止目錄遍歷) */
    private function resolvePath(string $file): ?string {
        // 規範化:移除多重斜杠、. 、..
        $file = ltrim($file, '/');
        $fullPath = $this->rootDir . DIRECTORY_SEPARATOR . $file;
        $realRoot = realpath($this->rootDir);
        $realFile = realpath($fullPath);

        // 必須在根目錄內、文件存在、擴展名白名單
        if (!$realFile || strpos($realFile, $realRoot) !== 0) {
            return null;
        }
        $ext = strtolower(pathinfo($realFile, PATHINFO_EXTENSION));
        if (!in_array($ext, ['js', 'css', 'map'])) {
            return null;
        }
        return $realFile;
    }

    /** 內容類型映射 */
    private function getContentType(string $ext): string {
        return match(strtolower($ext)) {
            'js'  => 'application/javascript',
            'css' => 'text/css',
            'map' => 'application/json',
            default => 'text/plain'
        };
    }

    /** 緩存頭:ETag 基于文件列表+修改時間,Last-Modified 取最新 */
    private function setCacheHeaders(array $files): void {
        $mtimes = [];
        foreach ($files as $file) {
            $path = $this->resolvePath($file);
            if ($path) $mtimes[] = filemtime($path);
        }
        $latest = $mtimes ? max($mtimes) : time();
        $etag = '"' . md5(implode(',', $files) . $latest) . '"';

        header('Cache-Control: public, max-age=31536000, immutable'); // 1 年,配合文件名哈希版本
        header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT');
        header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $latest) . ' GMT');
        header('ETag: ' . $etag);
        header('Vary: Accept-Encoding');

        // 304 Not Modified
        if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] === $etag) {
            http_response_code(304);
            exit;
        }
        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $latest) {
            http_response_code(304);
            exit;
        }
    }

    private function error(int $code, string $msg): void {
        http_response_code($code);
        header('Content-Type: application/json');
        echo json_encode(['error' => $msg]);
        exit;
    }
}

// ========== 入口文件 combo.php ==========
require_once __DIR__ . '/ComboService.php';
$combo = new ComboService(__DIR__ . '/public/static'); // 靜態資源根目錄
$combo->handle();

前端接入示例

<!-- HTML -->
<script src="/combo.php?f=lib/jquery-3.6.0.min.js,lib/vue.min.js,app/main.js,app/utils.js"></script>
<link rel="stylesheet" href="/combo.php?f=css/reset.css,css/layout.css,css/theme.css">

進階優化:構建時預合併 (現代工程化標配)

運行時 PHP 合併有 CPU/IO 開銷,**構建階段合併** 才是終極方案:

工具特點適用場景
Webpack / Vite / Rollup依賴圖分析、Tree Shaking、代碼分割、Hash 緩存現代前端項目 (Vue/React/Svelte)
Gulp / Grunt + concat/uglifycss簡單配置、流式處理傳統多頁應用、jQuery 時代遺留項目
Nginx ngx_http_concat_module模塊級高性能、無 PHP 開銷純靜態資源、不想引入構建工具
PHP Combo (本文方案)零構建依賴、動態組合、適合老系統快速接入legacy 系統、臨時優化、動態主題加載

Nginx 原生 concat 模塊配置 (最高性能)

# 編譯 Nginx 時添加 --add-module=../ngx_http_concat_module
location /static/ {
    concat on;
    concat_max_files 20;
    concat_unique off;  # 允許不同目錄文件合併
    concat_types text/css application/javascript;
    concat_delimiter "\n/* --- file boundary --- */\n";
}
# 前端調用:/static/??lib/jquery.js,lib/vue.js,app/main.js

避坑清單

  1. 文件編碼統一 UTF-8:合併前確保無 BOM,否則 JS 報錯/輸出亂碼
  2. 分號結尾:每文件末尾補分號或 PHP 輸出時強制換行隔離
  3. SourceMap 處理:合併會破壞 SourceMap,生產環境建議單獨提供 .map 或構建時內聯
  4. 緩存失效:文件內容變更需更新 URL (Query String ?v=hash 或 文件名 Hash)
  5. 跨域訪問:CDN 域名調用 Combo 需配置 Access-Control-Allow-Origin
  6. 大文件拆分:單文件 > 100KB 建議拆分或啟用 HTTP/2 推送,避免阻塞

總結

PHP 運行時合併是 「零重構、立竿見影」 的性能優化手段,適合存量系統快速提速。長期演進應遷移至 **構建時打包 + HTTP/2/3 + 瀏覽器緩存策略** 的現代化工程體系。
核心記住三點:
✅ 安全第一:路徑遍歷防護、擴展名白名單
✅ 緩存為王:ETag/Last-Modified + 長期強緩存 + 版本化 URL
✅ 監控到位:記錄合併請求耗時、命中率、錯誤率,持續優化

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved