减少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>
调用以上代码,我们的浏览器是这样显示的:
然后css目录生成了Pink.css文件:
img目录下生成了Pink.png文件:
看看生成的背景图是长啥样子:
接下来我们再看一下所生成的图片大小与Pink文件夹里所有小图片总和的大小,对它们做个比较:
从上图可以看出,我们生成的图片的大小明显小于文件夹所有图片的大小,所以在将100个小图标下载下来的速度 会明显小于 将背景图下载下来和将CSS下载下来的速度。
当访问量大时,或者小图片的量大时,会起到很明显的优化效果!!!
代码中的每一个点都基本上有注释,很方便大家去理解,只要大家用心去看,肯定能将这一网站优化技术用到自己的项目中。
本次博文就写到这!!!
如果此博文中有哪里讲得让人难以理解,欢迎留言交流,若有讲解错的地方欢迎指出。
如果您觉得您能在此博文学到了新知识,请为我顶一个,如文章中有解释错的地方,欢迎指出。
互相学习,共同进步!