Создавая мобильную версию одного крупного сервиса, задумался, а как можно ускорить загрузку страниц сайта. Придумал для себя несколько путей ускорения.

Оптимизация скорости загрузки сайта

  • Сжатие всех данных css, js, html gzip-ом
  • Сбор всех стилей и скриптов в два соответствующих файла. 
  • Установка времени сброса кеша на большой период.
  • Сбор всех иконок и т.п. графики в один графический файл, подобно тому, как это делает bootstrap
  • Кеширование генерированных страниц в файл, дабы потом не грузить mysql для неизменяемых данных
  • Общая оптимизация кода: js желательно подключать в конце страницы. Если используются like-кнопки различных сервисов, то лучше использовать код асинхронной загрузки, так как любой js тормозит прорисовку страницы до полной своей загрузки и выполнения. 

Для тестирования скорости загрузки я использовал FireBug аддон PageSpeed или его онлайн представление.

Сжатие всех данных css, js, htm

Существенно ускоряет загрузку, так как большинство браузеров уже поддерживают gzip сжатие на лету. Самое интересное, что сделать это достаточно просто: достаточно в файл htaccess своего сервера добавить несколько строк

AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/javascript text/css application/x-javascript
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4.0[678] no-gzip
BrowserMatch bMSIE !no-gzip !gzip-only-text/html

и все. Для Internet Explorer сжатие выключено, так как он его не поддерживает, для старых мазил тоже. 

В page speed видим, что скрипты и другие статичные файлы, в том числе html грузятся в меньшем объеме, отличном от фактического.

Сбор всех стилей и скриптов в два соответствующих файла

Логика тут проста: чем меньше файлов мы грузим тем быстрее. Загрузка 10 файлов по 1кб, медленнее чем одного по 10кб. Поэтому я написал функцию сбора

<?php 
define('ROOT',dirname(__FILE__).'/'); // корень сайта
/**
 * Компанует все подгружаемые скипты или стили в один файл, 
 * и заменяет все <script src=""></script> 
 * или <link type="text/css" rel="stylesheet" href=""/>
 * на одну запись <script src="onefile.php?file=[0-9a-z]{32}.js"></script> 
 * или <link type="text/css" rel="stylesheet" href="onefile.php?file=[0-9a-z]{32}.css"/>
 * 
 * @param $text html код из которого нужно осуществить сбор всех файлов
 * @param $type два варианта js или css
 * @param $file_except список файлов, которые не надо собирать разделенные |
 * @param $gzip_level степень gzip сжатия, если 0 то не сжимать.
 * @return результирующий html
 */
function compactFiles( $text,$type='js',$file_except = '', $gzip_level=7 ){
	$_mypth = $_pth = '/caches/scripts/';
	$_filexcept = explode('|',$file_except);
	$_filexcept = array_map('trim',$_filexcept);
	if( $type=='js' )
		$match ='#<script[^>]+src=("|\')([^"\']+)("|\').+</script>[\n\r\s]?#Uis'; 
	else
		$match = '#<link[^>]+href=("|\')([^"\']+)("|\')[^>]*>[\n\r\s]?#Uis';
	if( preg_match_all($match,$text,$slist) ){
		$buf=''; 
		$filegzipname = md5(implode('',$slist[2]));
		foreach($slist[2] as $kysrc => $src)
			if(in_array($src,$_filexcept)!=false){
				unset($slist[0][$kysrc]);
				unset($slist[1][$kysrc]);
				unset($slist[2][$kysrc]);
			}
		if( !file_exists(ROOT.$_pth) ){
			mkdir(ROOT.$_pth,0777);
		}
		foreach($slist[2] as $kysrc => $src){
			if(preg_match('#^http:\/\/#i',$src)){
				$filejs = file_get_contents($src);
			}else{
				if( is_readable( ROOT.$src ) != false ){
					$filejs = file_get_contents(ROOT.$src);
					if( $type=='css' ){
						if( preg_match_all('#url\(["\']?([^"\'\n\r\)]+)["\']?\)#Uis',$filejs,$urllist) ){
							$newurls = array();
							foreach( $urllist[1] as $url ){
								if( !preg_match('#^http:\/\/#i',$url) and !preg_match('#^[\/\\\\]+#i',$url) ){
									$newurls[$url] = str_replace(array(realpath(ROOT),'\\'),array('','/'),realpath(dirname(ROOT.$src).'/'.$url));
								}else $newurls[$url] = $url;
							}
							$filejs = strtr($filejs,$newurls);
						}
					}
				}else $filejs ='';
			}
			$buf.="$filejs\n";
		}
		file_put_contents( ROOT.$_pth.$filegzipname.'.'.$type,$buf );
		$gziped = (@extension_loaded('zlib')) ? 1 : 0;
		if( $gziped and $gzip_level){
			$buf = @gzencode($buf,$gzip_level);
			if(!file_exists(ROOT.$_pth.'gzip/')){
				@mkdir(ROOT.$_pth.'gzip/',0777);chmod(ROOT.$_pth.'gzip/',0777);
			}
			file_put_contents(ROOT.$_pth.'gzip/'.$filegzipname.'.'.$type,$buf);
		}
		if( count($slist[0]) ){
			$text = str_replace($slist[0][0],($type=='js')?'<script type="text/javascript" src="/onefile.php?file='.$filegzipname.'&type='.$type.'"></script>':'<link href="/onefile.php?file='.$filegzipname.'&type='.$type.'" type="text/css" rel="stylesheet" />',$text);
			$text = str_replace($slist[0],'',$text);
		}
	}
	return $text;
}

для ее работы еще потребуется доступный файл onefile.php

<?php 
define('ROOT',dirname(__FILE__).'/'); // корень сайта
function cmsCache_control($file, $time) {
	$etag = md5_file($file);
	$expr = 60 * 60 * 24 * 7;
	header("ETAG: " . $etag);
	header("LAST-MODIFIED: " . gmdate("D, d M Y H:i:s", $time) . " GMT");
	header("CACHE-CONTROL: ");
	header("PRAGMA: ");
	header("EXPIRES: ");
	if (isset($_SERVER["HTTP_IF_MODIFIED_SINCE"])) {
		$if_modified_since = preg_replace("/;.*$/", "", $_SERVER["HTTP_IF_MODIFIED_SINCE"]);
		if(trim($_SERVER["HTTP_IF_NONE_MATCH"]) == $etag && $if_modified_since == gmdate("D, d M Y H:i:s", $time). " GMT") {
			header("HTTP/1.0 304 Not modified");
			header("CACHE-CONTROL: MAX-AGE={$expr}, MUST-REVALIDATE");
			exit;
		}
	}
}

define('WORKDIR',ROOT.'/caches/scripts/');
$gzipenc = false;
if(strpos($_SERVER['HTTP_ACCEPT_ENCODING'],'x-gzip')!==false){ $gzipenc = 'x-gzip'; }
if(strpos($_SERVER['HTTP_ACCEPT_ENCODING'],'gzip')!==false){ $gzipenc = 'gzip'; }

if($filename = realpath(WORKDIR.$_GET['file'].'.'.$_GET['type'])){
	cmsCache_control($filename,filemtime($filename));
	@header('Content-Type:text/css;charset=utf8');
	if($gzipenc){
		@header("Content-Encoding: $gzipenc");
		echo file_get_contents(WORKDIR.'gzip/'.$_GET['file'].'.'.$_GET['type']);
	}else 
		echo file_get_contents($filename);
}

Использовать так:

<?php 
ob_start();
// генерируем наш html
echo '<script src="http://xdan.ru/jquery.js"></script>';
echo '<script src="jquery-ui.js"></script>';
echo '<script src="myfile.js"></script>';
echo '<script src="metrika.js"></script>';
echo '<link type="text/css" rel="stylesheet" href="bootstrap.css"/>';
echo '<link type="text/css" rel="stylesheet" href="myfile.css"/>';
echo '<body>
привет мир
</body>';
$html = compactFiles(ob_get_clean(),'js','metrika.js'); // собираем все скрипты кроме файла метрики
$html = compactFiles($html,'css'); // собираем все стили
echo $html;

в результате получим такой код

<script type="text/javascript" src="/onefile.php?file=0bafbf42f12a3a6e3c22c98e4d12aadf&type=js"></script>
<script src="metrika.js"></script>
<link href="/onefile.php?file=33166f9ca66a3436f24e20fe19421133&type=css" type="text/css" rel="stylesheet" />
<body>
привет мир
</body>

все файлы и папки Вам придется настроить под свой сервер 

Установка времени сброса кеша на большой период

У различных браузеров разные настройки проверки обновления статичных файлов ( фото, стили, скрипты). В большинстве случаев это сброс после закрытия браузера. Т.е. если человек будет заходить на Ваш сайт с утра, он каждый раз будет грузить все Ваши файлы. Чтобы этого не случилось достаточно установить в htaccess время жизни для кеш:

<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/x-icon A604800
ExpiresByType image/gif A604800
ExpiresByType image/jpeg A604800
ExpiresByType image/png A604800
ExpiresByType text/css A604800
ExpiresByType text/javascript A604800
ExpiresByType application/x-javascript A604800
</IfModule>

<ifModule mod_headers.c>
<filesMatch ".(ico|pdf|flv|jpg|jpeg|png|gif|swf|bmp)$">
Header set Cache-Control "max-age=604800, public"
</filesMatch>
<filesMatch ".(css|js)$">
Header set Cache-Control "max-age=604800, public"
</filesMatch>
</ifModule>

если хотите сделать это через php то смотрите функцию cmsCache_control из файла onefile.php выше

Сбор всех иконок и т.п. графики в один графический файл, подобно тому, как это делает bootstrap

Это лучше всего делать на этапе верстки. Называется техника css спрайты. Сходно по своей работе с предыдущим пунктом: загрузка 10 картинок размером 10кб, дольше чем одной в 100кб. Есть множество online сервисов для компановки изображений, погуглите "css sprite". 

Далее в своем css подключаем изображение в качестве фона, вот как это сделано в bootstrap

[class^="icon-"], [class*=" icon-"]  { 
background-image: url(icons-min.png) 0px 0px  no-repeat;
display: inline-block;
height: 16px;
line-height: 16px;
vertical-align: text-top;
width: 16px;
margin-top:-1px;
color:#fff;
text-align:center;
}

.icon-selector{background-position:0px 0px;}
.icon-triangle{background-position:-16px 0px;}
.icon-close{background-position:0px -16px;}
.icon-phone{background-position:0px -32px;}

для новых айфонов и айпадов есть смысл подключить картинки более высокой четкости, так как retina дисплеи все дела... четкость, никогда не бывает лишней

/** для ретина дисплеев **/
@media only screen and (-webkit-min-device-pixel-ratio: 1.5),
    only screen and (-o-min-device-pixel-ratio: 3/2),
    only screen and (min--moz-device-pixel-ratio: 1.5),
    only screen and (min-device-pixel-ratio: 1.5) {
   [class^="icon-"], [class*=" icon-"]  { 
	   background-image:url(icons-min2x.png);
	   background-size: 100% 100%;
   }
}

Кеширование генерированных страниц в файл, дабы потом не грузить mysql для неизменяемых данных

Эта техника применяется практически во всех современных CMS. К примеру UMI CMS, при генерации одной текстовой страницы может создавать свыше нескольких тысяч SQL запросов. Что сказывается на скорости ее работы. Поэтому после того, как страничка сгенерирована, ее можно сохранить в текстовый файл, а при последующем обращении брать данные из него. возьмем пример выше 

<?php 
define('ROOT',dirname(__FILE__).'/'); // корень сайта
$filecache = ROOT.'caches/'.md5( serialize($_GET) ).'.html'; // путь до нашего кеша
if( file_exists($filecache) and filemtime($filecache)>time()-3600*24 ){
	echo file_get_contents($filecache); // если кеш не старше одного дня то выводим его на экран
}else{
	ob_start();
	// генерируем наш html
	// миллион sql запросов
	echo '<body>
	привет мир
	</body>';
	$html = ob_get_clean();
	file_put_contents($filecache,$html);
}

если у вас динамичный сервис, с аяксом или формами, то имеет смысл такие запросы не кешировать, определить это можно так 

/**
* @desc функция проверки способа запроса страницы (возвращает: false-обычный, true-ajax) 
*/
function isAjax(){
	return isset($_SERVER['HTTP_X_REQUESTED_WITH']) and $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest';
}

тогда наш пример кеширования будет таким 

<?php 
define('ROOT',dirname(__FILE__).'/'); // корень сайта
$filecache = ROOT.'caches/'.md5( serialize($_GET) ).'.html'; // путь до нашего кеша
if( file_exists($filecache) and filemtime($filecache)>time()-3600*24 ){
    echo file_get_contents($filecache); // если кеш не старше одного дня то выводим его на экран
}else{
	ob_start();
	// генерируем наш html
	// миллион sql запросов
	echo '<body>
	привет мир
	</body>';
	$html = ob_get_clean();
	// если не ajax запрос и пост данные пусты
	if( !isAjax() and !count( $_POST ) )
		file_put_contents($filecache,$html);
}

Общая оптимизация кода

Если приведенную выше компановку js, подключать таким образом 

<script>
var d = document,
	n = d.getElementsByTagName("script")[0],
    s = d.createElement("script"),
    s.type = "text/javascript";
    s.async = true;
    s.src = "onefile.php?file=0bafbf42f12a3a6e3c22c98e4d12aadf&type=js";
n.parentNode.insertBefore(s, n);
</script>

это существенно ускорит загрузку страницы, так как прорисовка не будет зависеть от того загрузился ли весь js файл. Т.е. пользователь сможет читать текст, смотреть картинки, пока сайт будет грузить необходимые ему скрипты.

Есть еще не мало оптимизаций, которые вы могли бы проделать. СЕО тоже идет в счет, ведь поисковые системы тоже создают нагрузку. Немаловажную роль играет верная настройка robots.txt К примеру: поисковику неизвестно, что поисковую выдачу Вашего сайта индексировать не стоит, и он грузит все подряд, создавая нагрузку на сервер.

Большая получилась статья, надеюсь мои советы Вам пригодятся. Желаю удачи

Оставлять комментарии могут только зарегистрированные пользователи