減少HTTP請求之合并圖片詳解(大型網站優化技術)

jopen 9年前發布 | 16K 次閱讀 HTTP

 

一、相關知識講解

看過雅虎的前端優化35條建議,都知道優化前端是有多么重要。頁面的加載速度直接影響到用戶的體驗。80%的終端用戶響應時間都花在了前端上,其中大部分時間都在下載頁面上的各種組件:圖片,樣式表,腳本,Flash等等。

減少組件數必然能夠減少頁面提交的HTTP請求數。這是讓頁面更快的關鍵。減少頁面組件數的一種方式是簡化頁面設計。但有沒有一種方法可以在構建復雜的頁面同時加快響應時間呢?嗯,確實有魚和熊掌兼得的辦法。

這里我們就拿雅虎的第一條建議:盡量減少HTTP請求數里的減少圖片請求數量 進行講解。

我們都知道,一個網站的一個頁面可能有很多小圖標,例如一些按鈕、箭頭等等。當加載html文檔時,只要遇到有圖片的,都會自動建立起HTTP請 求下載,然后將圖片下載到頁面上,這些小圖片可能也就是十幾K大甚至1K都不到,假如我們的一個頁面有一百個小圖標,我們在加載頁面時,就要發送100個 HTTP請求,如果你的網站訪問量很大并發量也很高,假如上百萬訪問量,那發起的請求就是千萬級別了,服務器是有一定的壓力的,并且一個用戶的一個頁面要 發起那么多請求,是很耗時的。

所以,我們優化的方案就是:將這些十幾K、幾K的小圖標合并在一張圖片里,然后用CSS的background-image和background-position屬性來定位要顯示的部分。

、代碼實現

1、思路:

將一個文件夾里的圖標,自動生成在一張圖片里面,同時自動生成對應的css文件,我們只要在HTML里的標簽中添加相應的屬性值就能顯示圖片了。

2、實現過程:

XHTML

<?php
    //自己定義一個根目錄
    define('ROOT', $_SERVER['DOCUMENT_ROOT'].'iconwww');
    //這個是圖片的目錄
    define('RES_BASE_URL', 'http://localhost:8080/iconwww/img');

    /**
     * 生成背景圖的函數
     */
    function generateIcon() {
        //網站根目錄
        $webRoot = rtrim(ROOT, '/');
        //背景圖目錄
        $root = "$webRoot/img/bg";
        //Php-SPL庫中 的 目錄文件遍歷器
        $iterator = new DirectoryIterator($root);
        //開始遍歷該背景圖目錄下的目錄,我們是把想生成背景圖的目錄,放在bg目錄中以各個模塊的目錄分類存放
        foreach ($iterator as $file) {
            //遇到目錄遍歷
            if (!$file->isDot() && $file->isDir()) {
                //取得文件名
                $fileName = $file->getFilename();
                generateIconCallback("$root/$fileName", "$webRoot/img/$fileName", "$webRoot/css/$fileName.css");
            }
        }
    }

    /**
     * 用戶生成合并的背景圖和css文件的函數
     * @param  string $dir         生成背景圖的圖標所在的目錄路徑
     * @param  string $bgSavePath  背景圖所保存的路徑
     * @param  string $cssSavePath css保存的路徑
     */
    function generateIconCallback($dir, $bgSavePath, $cssSavePath) {
        $shortDir = str_replace('\\', '/', substr($dir, strlen(ROOT-1)));
        //返回文件路徑信息
        $pathInfo = pathinfo($bgSavePath.'.png');

        $bgSaveDir = $pathInfo['dirname'];
        //確保目錄可寫
        ensure_writable_dir($bgSaveDir);
        //背景圖名字
        $bgName = $pathInfo['filename'];
        //調用generateIconCallback_GetFileMap()函數生成每一個圖標所需要的數據結構
        $fileMap = array('a' => generateIconCallback_GetFileMap($dir));

        $iterator = new DirectoryIterator($dir);
        foreach ($iterator as $file) {
            if ($file->isDot()) continue;
            if ($file->isDir()) {
                //二級目錄也要處理
                $fileMap['b-'.$file->getFilename()] = generateIconCallback_GetFileMap($file->getRealPath());
            } 
        }
        ksort($fileMap);

        //分析一邊fileMap,計算整個背景圖的大小和每一個圖標的offset
        //初始化偏移量和背景圖    
        $offsetX = $offsetY = $bgWidth = 0;
        //設定每個小圖標之間的距離
        $spaceX =$spaceY = 5;
        //圖片最大寬度
        $maxWidth = 800;
        $fileMd5List =array();
        //這里需要打印下$fileMap就知道它的數據結構了
        foreach ($fileMap as $k1 => $innerMap) {
            foreach ($innerMap as $k2 => $itemList) {
                //行高姐X軸偏移量初始化
                $offsetX = $lineHeight = 0;
                foreach ($itemList as $k3 => $item) {
                    //變量分別是:圖標的寬度,高度,類型,文件名,路徑,MD5加密字符串
                    list($imageWidth, $imageHeight, $imageType, $fileName, $filePathname, $fileMd5) = $item;
                    $fileMd5List []= $fileMd5;
                    //如果圖片的寬度+偏移量 > 最大寬度(800) 那就換行
                    if ($offsetX !== 0 && $imageWidth + $offsetX > $maxWidth) {
                        $offsetY += $spaceY + $lineHeight;
                        $offsetX = $lineHeight = 0;
                    }
                    //如果圖片高度 > 當前行高  那就講圖片高度付給行高我們這的
                    if ($imageHeight > $lineHeight) $lineHeight = $imageHeight;
                    $fileMap[$k1][$k2][$k3] = array($imageWidth, $imageHeight, $offsetX, $offsetY, $imageType, $fileName, $filePathname);
                    //X軸偏移量的計算
                    $offsetX += $imageWidth + $spaceX;
                    if ($offsetX > $bgWidth) $bgWidth = $offsetX;
                }
                //Y軸偏移量的計算
                $offsetY +=  $lineHeight + $spaceY;
            }
        }
        //把右下兩邊多加了的空白距離給干掉
        $bgWidth -= $spaceX;
        $bgHeight = $offsetY - $spaceY;
        $fileMd5List = implode("\n", $fileMd5List);

        //生成背景圖和 css文件

        //資源路徑
        $resBaseUrl = RES_BASE_URL;
        $suffix = base_convert(abs(crc32($fileMd5List)), 10, 36);
        $writeHandle = fopen($cssSavePath, 'w');
        fwrite($writeHandle, "/** bg in dir: $shortDir/ */\n[icon-$bgName]{background:url({$resBaseUrl}/$bgName.png?$suffix) no-repeat;display:inline-block;}");

        //做圖片,這些函數具體可以查看PHP手冊
        $destResource = imagecreatetruecolor($bgWidth, $bgHeight);
        imagealphablending($destResource, false);
        imagesavealpha($destResource, false);
        $color = imagecolorallocatealpha($destResource, 255, 255, 255, 127);

        imagefill($destResource, 0, 0, $color);

        //對每一張小圖片進行處理,生成在大背景圖里,并生成css文件
        foreach ($fileMap as $innerMap) {
            foreach ($innerMap as $itemList) {
                foreach ($itemList as $item) {
                     list($imageWidth, $imageHeight, $offsetX, $offsetY, $imageType, $fileName, $filePathname) = $item;
                     if ($imageType === IMAGETYPE_PNG) {
                        $srcResource = imagecreatefrompng($filePathname);
                     } else if ($imageType === IMAGETYPE_JPEG) {
                        $srcResource = imagecreatefromjpeg($filePathname);
                     }
                     imagecopy($destResource, $srcResource, $offsetX, $offsetY, 0, 0, $imageWidth, $imageHeight);
                     imagedestroy($srcResource);

                     //寫入css
                     $posX = $offsetX === 0 ? 0 : "-{$offsetX}px";
                     $posY = $offsetY === 0 ? 0 : "-{$offsetY}px";
                     fwrite($writeHandle, "\n[icon-$bgName=\"$fileName\"]{width:{$imageWidth}px;height:{$imageHeight}px;background-position:$posX $posY;}");
                 } 
            }
        }

        //壓縮級別 7
        imagepng($destResource, "$bgSavePath.png", 7);
        imagedestroy($destResource);
        fclose($writeHandle);

        $shortCssSavePath = substr($cssSavePath, strlen(ROOT));
    }

    /**
     * 將圖片的信息處理成我們想要的數據結構
     * @param  [type] $dir [description]
     * @return [type]      [description]
     */
    function generateIconCallback_GetFileMap($dir) {
        $map = $sort = array();
        $iterator = new DirectoryIterator($dir);
        foreach($iterator as $file) {
            if(!$file->isFile()) continue;
            $filePathname = str_replace("\\", '/', $file->getRealPath());
            //這些函數可以查看PHP手冊
            $imageInfo = getimagesize($filePathname);
            $imageWidth = $imageInfo[0];
            $imageHeight = $imageInfo[1];
            $imageType = $imageInfo[2];

            if(!in_array($imageType, array(IMAGETYPE_JPEG, IMAGETYPE_PNG))) {
                $fileShortName = substr($filePathname, strlen(ROOT) - 1);
                echo "<p> $fileShortName 圖片被忽略: 因為圖片類型不是png|jpg.</p>";
                continue;
            }

            //這是我們的圖片規格,行高分別有 16 32 64 128 256 99999 
            foreach(array(16, 32, 64, 128, 256, 99999) as $height) {
                if($imageHeight <= $height) {
                    $mapKey = $height;
                    break;
                }
            }
            if(!isset($map[$mapKey])) $map[$mapKey] = array();
            $filePathInfo = pathinfo($filePathname);
            $map[$mapKey] []= array($imageWidth, $imageHeight, $imageType, $filePathInfo['filename'], $filePathname, md5_file($filePathname));
            $sort[$mapKey] []= str_pad($imageHeight, 4, '0', STR_PAD_LEFT) . $filePathInfo['filename'];
        }
        foreach($map as $k => $v) array_multisort($map[$k], SORT_ASC, SORT_NUMERIC, $sort[$k]);
        ksort($map, SORT_NUMERIC);
        return $map;
    }

    /**
     * 判斷目錄是否可寫
     * @param  string $dir 目錄路徑
     */
    function ensure_writable_dir($dir) {
        if(!file_exists($dir)) {
            mkdir($dir, 0766, true);
            @chmod($dir, 0766);
            @chmod($dir, 0777);
        }
        else if(!is_writable($dir)) {
            @chmod($dir, 0766);
            @chmod($dir, 0777);
            if(!@is_writable($dir)) {
                throw new BusinessLogicException("目錄不可寫", $dir);
            }
        }
    }

    generateIcon();
?>
<!DOCTYPE html>
<html>
<head>
    <link rel="stylesheet" type="text/css" href="css/Pink.css">
    <title></title>

</head>
<body>
<div>我們直接引入所生成的css文件,并測試一下是否成功</div>
<br>
<div>這里在span標簽 添加屬性 icon-Pink ,值為About-40,正常顯示圖片</div>
<span icon-Pink="About-40"></span>
</body>
</html>

<?php
  //自己定義一個根目錄
  define('ROOT',$_SERVER['DOCUMENT_ROOT'].'iconwww');
  //這個是圖片的目錄
  define('RES_BASE_URL','http://localhost:8080/iconwww/img');
  /**
   * 生成背景圖的函數
   */
  functiongenerateIcon(){
    //網站根目錄
    $webRoot=rtrim(ROOT,'/');
    //背景圖目錄
    $root="$webRoot/img/bg";
    //Php-SPL庫中 的 目錄文件遍歷器
    $iterator=newDirectoryIterator($root);
    //開始遍歷該背景圖目錄下的目錄,我們是把想生成背景圖的目錄,放在bg目錄中以各個模塊的目錄分類存放
    foreach($iteratoras$file){
      //遇到目錄遍歷
      if(!$file->isDot()&&$file->isDir()){
        //取得文件名
        $fileName=$file->getFilename();
        generateIconCallback("$root/$fileName","$webRoot/img/$fileName","$webRoot/css/$fileName.css");
      }
    }
  }
  /**
   * 用戶生成合并的背景圖和css文件的函數
   * @param  string $dir         生成背景圖的圖標所在的目錄路徑
   * @param  string $bgSavePath  背景圖所保存的路徑
   * @param  string $cssSavePath css保存的路徑
   */
  functiongenerateIconCallback($dir,$bgSavePath,$cssSavePath){
    $shortDir=str_replace('\\','/',substr($dir,strlen(ROOT-1)));
    //返回文件路徑信息
    $pathInfo=pathinfo($bgSavePath.'.png');
    $bgSaveDir=$pathInfo['dirname'];
    //確保目錄可寫
    ensure_writable_dir($bgSaveDir);
    //背景圖名字
    $bgName=$pathInfo['filename'];
    //調用generateIconCallback_GetFileMap()函數生成每一個圖標所需要的數據結構
    $fileMap=array('a'=>generateIconCallback_GetFileMap($dir));
    $iterator=newDirectoryIterator($dir);
    foreach($iteratoras$file){
      if($file->isDot())continue;
      if($file->isDir()){
        //二級目錄也要處理
        $fileMap['b-'.$file->getFilename()]=generateIconCallback_GetFileMap($file->getRealPath());
      }
    }
    ksort($fileMap);
    //分析一邊fileMap,計算整個背景圖的大小和每一個圖標的offset
    //初始化偏移量和背景圖    
    $offsetX=$offsetY=$bgWidth=0;
    //設定每個小圖標之間的距離
    $spaceX=$spaceY=5;
    //圖片最大寬度
    $maxWidth=800;
    $fileMd5List=array();
    //這里需要打印下$fileMap就知道它的數據結構了
    foreach($fileMapas$k1=>$innerMap){
      foreach($innerMapas$k2=>$itemList){
        //行高姐X軸偏移量初始化
        $offsetX=$lineHeight=0;
        foreach($itemListas$k3=>$item){
          //變量分別是:圖標的寬度,高度,類型,文件名,路徑,MD5加密字符串
          list($imageWidth,$imageHeight,$imageType,$fileName,$filePathname,$fileMd5)=$item;
          $fileMd5List[]=$fileMd5;
          //如果圖片的寬度+偏移量 > 最大寬度(800) 那就換行
          if($offsetX!==0&&$imageWidth+$offsetX>$maxWidth){
            $offsetY+=$spaceY+$lineHeight;
            $offsetX=$lineHeight=0;
          }
          //如果圖片高度 > 當前行高  那就講圖片高度付給行高我們這的
          if($imageHeight>$lineHeight)$lineHeight=$imageHeight;
          $fileMap[$k1][$k2][$k3]=array($imageWidth,$imageHeight,$offsetX,$offsetY,$imageType,$fileName,$filePathname);
          //X軸偏移量的計算
          $offsetX+=$imageWidth+$spaceX;
          if($offsetX>$bgWidth)$bgWidth=$offsetX;
        }
        //Y軸偏移量的計算
        $offsetY+=  $lineHeight+$spaceY;
      }
    }
    //把右下兩邊多加了的空白距離給干掉
    $bgWidth-=$spaceX;
    $bgHeight=$offsetY-$spaceY;
    $fileMd5List=implode("\n",$fileMd5List);
    //生成背景圖和 css文件
    //資源路徑
    $resBaseUrl=RES_BASE_URL;
    $suffix=base_convert(abs(crc32($fileMd5List)),10,36);
    $writeHandle=fopen($cssSavePath,'w');
    fwrite($writeHandle,"/** bg in dir: $shortDir/ */\n[icon-$bgName]{background:url({$resBaseUrl}/$bgName.png?$suffix) no-repeat;display:inline-block;}");
    //做圖片,這些函數具體可以查看PHP手冊
    $destResource=imagecreatetruecolor($bgWidth,$bgHeight);
    imagealphablending($destResource,false);
    imagesavealpha($destResource,false);
    $color=imagecolorallocatealpha($destResource,255,255,255,127);
    imagefill($destResource,0,0,$color);
    //對每一張小圖片進行處理,生成在大背景圖里,并生成css文件
    foreach($fileMapas$innerMap){
      foreach($innerMapas$itemList){
        foreach($itemListas$item){
           list($imageWidth,$imageHeight,$offsetX,$offsetY,$imageType,$fileName,$filePathname)=$item;
           if($imageType===IMAGETYPE_PNG){
            $srcResource=imagecreatefrompng($filePathname);
           }elseif($imageType===IMAGETYPE_JPEG){
            $srcResource=imagecreatefromjpeg($filePathname);
           }
           imagecopy($destResource,$srcResource,$offsetX,$offsetY,0,0,$imageWidth,$imageHeight);
           imagedestroy($srcResource);
           //寫入css
           $posX=$offsetX===0?0:"-{$offsetX}px";
           $posY=$offsetY===0?0:"-{$offsetY}px";
           fwrite($writeHandle,"\n[icon-$bgName=\"$fileName\"]{width:{$imageWidth}px;height:{$imageHeight}px;background-position:$posX $posY;}");
         }
      }
    }
    //壓縮級別 7
    imagepng($destResource,"$bgSavePath.png",7);
    imagedestroy($destResource);
    fclose($writeHandle);
    $shortCssSavePath=substr($cssSavePath,strlen(ROOT));
  }
  /**
   * 將圖片的信息處理成我們想要的數據結構
   * @param  [type] $dir [description]
   * @return [type]   [description]
   */
  functiongenerateIconCallback_GetFileMap($dir){
    $map=$sort=array();
    $iterator=newDirectoryIterator($dir);
    foreach($iteratoras$file){
      if(!$file->isFile())continue;
      $filePathname=str_replace("\\",'/',$file->getRealPath());
      //這些函數可以查看PHP手冊
      $imageInfo=getimagesize($filePathname);
      $imageWidth=$imageInfo[0];
      $imageHeight=$imageInfo[1];
      $imageType=$imageInfo[2];
      if(!in_array($imageType,array(IMAGETYPE_JPEG,IMAGETYPE_PNG))){
        $fileShortName=substr($filePathname,strlen(ROOT)-1);
        echo"<p> $fileShortName 圖片被忽略: 因為圖片類型不是png|jpg.</p>";
        continue;
      }
      //這是我們的圖片規格,行高分別有 16 32 64 128 256 99999
      foreach(array(16,32,64,128,256,99999)as$height){
        if($imageHeight<=$height){
          $mapKey=$height;
          break;
        }
      }
      if(!isset($map[$mapKey]))$map[$mapKey]=array();
      $filePathInfo=pathinfo($filePathname);
      $map[$mapKey][]=array($imageWidth,$imageHeight,$imageType,$filePathInfo['filename'],$filePathname,md5_file($filePathname));
      $sort[$mapKey][]=str_pad($imageHeight,4,'0',STR_PAD_LEFT).$filePathInfo['filename'];
    }
    foreach($mapas$k=>$v)array_multisort($map[$k],SORT_ASC,SORT_NUMERIC,$sort[$k]);
    ksort($map,SORT_NUMERIC);
    return$map;
  }
  /**
   * 判斷目錄是否可寫
   * @param  string $dir 目錄路徑
   */
  functionensure_writable_dir($dir){
    if(!file_exists($dir)){
      mkdir($dir,0766,true);
      @chmod($dir,0766);
      @chmod($dir,0777);
    }
    elseif(!is_writable($dir)){
      @chmod($dir,0766);
      @chmod($dir,0777);
      if(!@is_writable($dir)){
        thrownewBusinessLogicException("目錄不可寫",$dir);
      }
    }
  }
  generateIcon();
?>
<!DOCTYPE html>
<html>
<head>
  <linkrel="stylesheet"type="text/css"href="css/Pink.css">
  <title></title>
</head>
<body>
<div>我們直接引入所生成的css文件,并測試一下是否成功</div>
<br>
<div>這里在span標簽 添加屬性 icon-Pink ,值為About-40,正常顯示圖片</div>
<span icon-Pink="About-40"></span>
</body>
</html>

調用以上代碼,我們的瀏覽器是這樣顯示的:

減少HTTP請求之合并圖片詳解(大型網站優化技術)

然后css目錄生成了Pink.css文件:

減少HTTP請求之合并圖片詳解(大型網站優化技術)

img目錄下生成了Pink.png文件:

減少HTTP請求之合并圖片詳解(大型網站優化技術)

看看生成的背景圖是長啥樣子:

減少HTTP請求之合并圖片詳解(大型網站優化技術)

接下來我們再看一下所生成的圖片大小與Pink文件夾里所有小圖片總和的大小,對它們做個比較:

減少HTTP請求之合并圖片詳解(大型網站優化技術)

減少HTTP請求之合并圖片詳解(大型網站優化技術)

從上圖可以看出,我們生成的圖片的大小明顯小于文件夾所有圖片的大小,所以在將100個小圖標下載下來的速度 會明顯小于 將背景圖下載下來和將CSS下載下來的速度。

當訪問量大時,或者小圖片的量大時,會起到很明顯的優化效果!!!

代碼中的每一個點都基本上有注釋,很方便大家去理解,只要大家用心去看,肯定能將這一網站優化技術用到自己的項目中。

本次博文就寫到這!!!

如果此博文中有哪里講得讓人難以理解,歡迎留言交流,若有講解錯的地方歡迎指出。

如果您覺得您能在此博文學到了新知識,請為我頂一個,如文章中有解釋錯的地方,歡迎指出。

互相學習,共同進步!

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!