Создавая мобильную версию одного крупного сервиса, задумался, а как можно ускорить загрузку страниц сайта. Придумал для себя несколько путей ускорения.
- Сжатие всех данных 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 К примеру: поисковику неизвестно, что поисковую выдачу Вашего сайта индексировать не стоит, и он грузит все подряд, создавая нагрузку на сервер.
Большая получилась статья, надеюсь мои советы Вам пригодятся. Желаю удачи