Оптимизация сайта на MODX

Мне дали задание оптимизировать сайт https://mebmart.by для https://developers.google.com/speed/pagespeed/insights/. Я с задачей оптимизации сталкиваюсь первый раз. К сожалению, инструкций по оптимизации сайта на MODX не нашел. Поэтому хочу сделать такую инструкцию. Надеюсь, в комментариях, более опытные разработчика подскажут что и как еще можно сделать.

PageSpeed Insights мне написал:
Оптимизация
Low
11 / 100

Предложения по оптимизации
Оптимизируйте изображения
Используйте кеш браузера
Сократите время ответа сервера
Удалите код JavaScript и CSS, блокирующий отображение верхней части страницы
Сократите JavaScript
Сократите CSS
Сократите HTML
Включите сжатие
Для Сократите JavaScript, Сократите CSS использую MinifyX.
Для Сократите HTML нашел плагин minifyHTML на событие OnWebPagePrerender:
<?php
$output = $modx->resource->_output;
$output= preg_replace('|\s+|', ' ', $output);
$modx->resource->_output = $output;
Следующий пункт за который взялся Сократите время ответа сервера. PageSpeed Insights пишет:
По результатам тестирования время ответа вашего сервера составило 0,42 секунды.
И рекомендованное время ответа сервера меньше 200 мс.
Поставил компонент debugParser. Захожу на адрес mebmart.by/?debug=1 и вижу следующею раскладку:

Total parse time 0.4579089 s
Total queries 129
Total queries time 0.0209119 s
(Надо было запускать mebmart.by/?debug=1&cache=1 чтобы считало с кешом. Спасибо комментаторам.)

202мс занимает только вызов pdoPage. Как оказалось, на главной выводятся картинки товаров с функцией БЫСТРЫЙ ПРОСМОТР. А для того чтобы эта функция работала в чанке tpl.msProducts.row выводится сразу модальное окно с просмотром товара:
<li>
					<div class="block_img">
						<a class="wrap_img" href="[[~[[+id]]]]">
	[[+new:is=`1`:then=`<span class="title">Новинка</span>`]]
    [[+popular:is=`1`:then=`<span class="title">Хит Продаж</span>`]]
    [[+favorite:is=`1`:then=`<span class="title">Новогодняя цена</span>`]]
							<img src="[[+thumb:default=`[[++assets_url]]components/minishop2/img/web/ms2_small.png`]]" width="2149" height="1440" alt="[[+pagetitle]]"/>
						</a>
						<a href="#product_popup[[+id]]" class="more fancybox">БЫСТРЫЙ ПРОСМОТР</a>
					</div>
					<h3><a href="[[~[[+id]]]]">[[+pagetitle]]</a></h3>
					<div class="price">[[+price]] бел.руб. </div>

					<!-- product_popup -->
					<div class="product_popup" id="product_popup[[+id]]">

						<!-- product_page -->
						<div class="product_page">

							<!-- wrap_product -->
							<div class="wrap_product">
								
								<div class="main_img zoom"><img src="[[+image:default=`[[++assets_url]]components/minishop2/img/web/ms2_small.png`]]" width="2149" height="1440" alt=""/></div>
								<ul class="thumbs">
									[[!pdoResources?
	&class=`msProductFile`
	&sortby=`id`
	&sortdir=`asc`
	&where=`{"product_id":"[[+id]]","type:=":"image","path:=":"[[+id]]/1500x1005/"}`
	&tpl=`@INLINE <li><img src="[[+url]]"></li>`
]]
								</ul>
							</div>
							<!-- /wrap_product -->

							<!-- options -->
							<div class="options">
								<h2 class="title_tovar">[[+pagetitle]]</h2>
								<div class="price">[[+price]] бел.руб <a href="[[~[[+id]]]]">Подробнее о товаре</a></div>
		<form method="post" class="ms2_form">						
								<div class="count">
									<span>Количество</span>
									<select name="count">
										<option value="1">1</option>
										<option value="2">2</option>
										<option value="3">3</option>
										<option value="4">4</option>
										<option value="5">5</option>
										<option value="6">6</option>
									</select>
								</div>
							
 <button class="add_to_cart btn" type="submit" name="ms2_action" value="cart/add">ДОБАВИТЬ В КОРЗИНУ</button>

		<input type="hidden" name="id" value="[[+id]]">
		<input type="hidden" name="options" value="[]">
		
		</form>

							</div>
							<!-- /options -->

						</div>
						<!-- /product_page -->

					</div>
					<!-- /product_popup -->

				</li>
Скорость pdoPage падает из-за вызова внутри чанка сниппета pdoResources. И к тому же по 5мс 18 раз тратится на вызов pdoResources.
Для того чтобы преодолеть эту проблему решил подгружать модальное окно БЫСТРЫЙ ПРОСМОТР через ajax. Так как функция думаю востребованная написал сразу компонент AjaxModal. После этого скорость pdoPage упала до 88мс.

Total parse time 0.3473721 s
Total queries 92
Total queries time 0.0194972 s

PageSpeed Insights показывает
По результатам тестирования время ответа вашего сервера составило 0,34 секунды.
Уменьшилось на 80мс, но все равно много :(. На этом мои успехи пока закончились.

Почему-то для pdoPage установка параметров &cache=`1` &cacheTime=`3600` &context=`web` никак не влияют на скорость ответа. Возможно из-за &ajaxMode=`button`. При вызове pdoPage кешируемым скорость тоже не падает и к тому же пропадает кнопка показать еще.
Следующим этапом, думаю, перевести по крайней мере главную страницу на феном. Все его хвалят за скорость вот и посмотрим насколько он быстрее.
Более опытные пожалуйста скажите как еще можно оптимизировать сайт. И, кстати, сколько стоит оптимизация сайта? Мне сейчас предложили маленькую сумму, а чтоб ее увеличить нужно аргументы :).

UPD 07.08.18
pdoPage отрабатывает за 88мс и явно не кешируется. Полез разбираться и обнаружил 2 ошибки с кешем в нем.
Сделал вывод имен файлов кеша. Запрашиваемое имя и записываемое. Обнаружил:
No cached data for key "resource/web/resources/89/4e61020450c4424d73d66aee8a9324bfe0a70960"
cacheKey web/resources/89/4ccd1f1b0b32e54c8ce591eca2270b39c739fdfc
Запрашивается имя кеша одно, а записывается другое. Нашел, что имя кеша генерируется в файле core/components/pdotools/model/pdotools/pdotools.class.php функция protected function getCacheKey($options = array())
return $key . '/' . sha1(serialize($options));
где $options — это $scriptProperties сниппета. Для запрашиваемого и записываемого имени кеша $scriptProperties в функцию попадают разные. В сниппете pdoPage
$data = $cache
    ? $pdoPage->pdoTools->getCache($scriptProperties)
    : array();
//echo "<pre>".print_r($data,1)."</pre>";

if (empty($data)) {
    $scriptProperties['setTotal'] = true;
то есть после запроса кеша в $scriptProperties записывается setTotal и соответственно генерируемое имя файла кеша изменяется. Для обхода этой ошибки в функции getCacheKey перед генерацией имени файла кеша сделал
unset($options['setTotal']);
return $key . '/' . sha1(serialize($options));
Вторая ошибка: в $scriptProperties попадают параметры индивидуальные для каждого браузера
[request] => Array
        (
            [browser] => standard
            [modx_setup_language] => ru
            [_ga] => GA1.2.1805674015.1532178758
            [_ym_uid] => 1532178758631790336
            [_ym_d] => 1532178758
            [minishop2-category-grid-1] => {"start":0,"limit":"","action":"mgr/product/getlist","parent":"1"}
            [minishop2-category-grid-90] => {"start":0,"limit":"","action":"mgr/product/getlist","parent":"90"}
            [minishop2-category-grid-93] => {"start":0,"limit":"","action":"mgr/product/getlist","parent":"93"}
            [PHPSESSID] => 1f01ac36**********************************
            [_gid] => GA1.2.907764301.1533494910
            [_ym_isad] => 1
        )
То есть для каждого браузера генерируется отдельный кеш. Смысл кеша от этого пропадает :).
Добавил
unset($options['setTotal']);
        unset($options['request']);
        return $key . '/' . sha1(serialize($options));
В итоге pdoPage из кеша грузиться 9мс и Total parse time 0.1053231 s. Под требования гугл проходит. Переводить на феном трудозатратно и большого смысла нет.

UPD 13.08.18
Взялся за оптимизацию изображений на главной. Сперва пробую tinyCompressor. Написал сниппет-модификатор [[+image_absolute:phpthumbon=`w=476&h=318&zc=1`:tinyCacheImage]], чтобы обрабатывать изображения во время первой загрузки страницы. Поправил ошибки в tinyCompressor https://modx.pro/components/14066#comment-100636. Тестирую и тут выясняется, что картинки, сжатые TinyPNG, не проходят в PageSpeed Insights. Он пишет нужно сжать еще.

В итоге, пробую другой компонент OptiPic. Сперва не заработал, но выяснилось, что я просто не весь ключ АПИ скопировал. В чанке my.galItemThumb, который для Gallery, сделал
<div class="[[+cls]]">
    <a href="[[+url]]" title="[[+name]]" [[+link_attributes]]>

        <img class="[[+imgCls]]" src="[[+image_absolute:phpthumbon=`w=476&h=318&zc=1`:optipic]]" alt="[[+name]]" [[+image_attributes]] />
    </a>
</div>
И, теперь, PageSpeed Insights на эти файлы уже не ругается :). Так же оптимизировал картинки для pdoPage. Тут пришлось докупить мегабайты на optipic.io. Но 1гб за 225р — это по моему не дорого. Тестирую и вижу, что гугл оценивает уже 77 баллов из 100. От куда-то взялись не оптимизированные картинки с разрешением 1000х670. В вызове pdoPage делаю &includeThumbs=`298x200` и в чанке tpl.msProducts.row
<img src="[[+298x200:default=`[[++assets_url]]components/minishop2/img/web/ms2_small.png`:optipic]]" alt="[[+pagetitle]]"/>
Тестирую и уже


Вывод надо было начинать с оптимизации картинок :). Они очень сильно влияют на оценку гугла. Все остальное уже в пределах 10 баллов.

Главная страница практически оптимизированна. Осталось оптимизировать категории и товары. И тут выяснялась засада :). Многие превью с разрешением 298х200 битые. Сейчас их перегенерирую.
В результате экспериментов на основе https://modx.pro/help/8244#comment-91916 сделал скрипт перегенерации превью
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);

require_once dirname(__FILE__).'/config.core.php';
require_once MODX_CORE_PATH.'config/'.MODX_CONFIG_KEY.'.inc.php';
require_once MODX_CONNECTORS_PATH.'index.php';

$corePath = $modx->getOption('minishop2.core_path',null,$modx->getOption('core_path').'components/minishop2/');
require_once $corePath.'model/minishop2/minishop2.class.php';
$modx->miniShop2 = new miniShop2($modx);

$modx->lexicon->load('minishop2:default');
$c = $modx->newQuery('msProduct');
$c->where(array('class_key' => 'msProduct'));

$offset = $_GET['offset'];
if(!$offset) $offset=0;
$count = $modx->getCount('msProduct', $c);
$c->limit(5,$offset); 
echo "$offset from $count
";
$offset += 5;


$products = $modx->getIterator('msProduct', $c);
$i = 0;
foreach ($products as $product) {
    //if($i >10) break;
    $i++;
    // Получаем оригиналы их картинок
    $files = $product->getMany('Files', array('parent' => 0));
    foreach ($files as $file) {
        //echo print_r($file->toArray(),1)."
";
		// Затем получаем их преью
        $children = $file->getMany('Children');
        foreach ($children as $child) {
            //echo print_r($child->toArray(),1)."
";
			
			//удаляем файлы optipic
			$fullpath = MODX_BASE_PATH . $child->url;
			$path_arr = explode('/', $fullpath);
			$file_name = array_pop($path_arr);
			$optimized_file_name = 'op-' . $file_name;
			$optimized_file = implode('/', $path_arr) . '/' . $optimized_file_name;
			echo $optimized_file."
";
			@unlink($optimized_file);
			
			// Удаляем эти превью, вместе с файлами
			$child->remove();
        }
        // И генерируем новые
        $file->generateThumbnails();

        // Если это первый файл в галерее - обновляем ссылку на превью товара
        /** @var msProductData $data */
        if ($file->get('rank') == 0 && $data = $product->getOne('Data')) {
            $thumb = $file->getFirstThumbnail();
            $data->set('thumb', $thumb['url']);
            $data->save();
        }
    }
}
if($offset > $count) exit;
?>
<script>
location.href = "?offset=<?echo $offset;?>";
</script>
На каталоге и категориях 89-92 балла. Осталось оптимизировать страницы товаров. Сейчас на них 42 балла. В первую очередь меняю превью галереи, которые грузились в полный размер 1000х670, меняю на 41х41. my.tpl.msGallery.row
<li class="active"><img src="[[+41x41]]" data-src="[[+1000x670]]" alt="[[+name]]"/></li>
И меняю ява-скрипт, чтобы при клике на превью в главную картинку прогружалось из data-src.
Затем главную картинку гружу через optipic
<div class="main_img zoom"><img src="[[+image:default=`[[++assets_url]]components/minishop2/img/web/ms2_small.png`:optipic]]" width="2149" height="1440" alt=""/></div>
В итоге, на странице товара 84 / 100 good. Только почему-то время ответа сервера 1,2 секунды :(. В debugParser
Total parse time 0.1609571 s. И откуда целая секунда берется не понятно :(.
Для того, чтобы все картинки сайта прогрузились и закешировались прогнал сайт через Screaming Frog SEO Spider.

В целом, все страницы в PageSpeed Insights набирают Good 80-94 балла. На оптимизацию потратил 30 с чем-то часов.
В следующий раз, думаю, оптимизирую гораздо быстрей. И, надеюсь, данная статья сократит работу над оптимизацией для Вас.
Александр
06 августа 2018, 00:08
16
1 239
+5

Комментарии: 24

Алексей Ерохин
06 августа 2018, 01:36
+1
js и css от компонентов можно тоже в minifyx загнать (не забыть отключить их в настройках или в вызове)

Ну и картинки. Скачайте, что вам гугл предлагает, посмотрите на качество.
Это очень плохо yadi.sk/i/rvBAyF2g3ZvLEq
Режьте нужные размеры при загрузке (как в ms2gallery или userfiles)
Прогоните изображения шаблона (типа таких mebmart.by/search.png) через tinypng.com/, например
Не работал с Gallery, но на каждую картинку слайдера идет запрос на коннектор — отсюда короткое время кеширования. может есть там возможность уже готовый статичный путь получать?
    Александр
    06 августа 2018, 07:18
    0
    Спасибо за ответ.
    js и css от компонентов можно тоже в minifyx загнать (не забыть отключить их в настройках или в вызове)
    До этого еще просто не добрался. Завис на скорости ответа сайта.
    Это очень плохо yadi.sk/i/rvBAyF2g3ZvLEq

    Это вообще не понятно зачем такое. В модальном окне зум работает. Наверно разраб сайта под одну гребенку все сделал.
    Не работал с Gallery, но на каждую картинку слайдера идет запрос на коннектор — отсюда короткое время кеширования. может есть там возможность уже готовый статичный путь получать?
    Тоже удивило. Здесь еще не копался. Только стандартные параметры сниппета смотрел. Наверно напишу свой сниппет для вывода галереи. А то стандартный сниппет работает аж 50мс. Что он там делает столько времени не понятно :(.
    Пока больше интересует скорость работы. Кто-нибудь замерял насколько феном убыстряет?
Павел Гвоздь
06 августа 2018, 07:26
+2
Почему-то для pdoPage установка параметров &cache=`1` &cacheTime=`3600` &context=`web` никак не влияют на скорость ответа.
Если замеры делаются через debugParser, то при вызове страницы с GET параметром
&debug=1
страница отдаётся некешированной, насколько мне известно. Чтобы замерить с кешем, то
&debug=1&cache=1
    Александр
    06 августа 2018, 07:55
    0
    Спасибо не знал или уже забыл.
    pdoPage все равно 88-93мс занимает. Его надо еще сокращать. Правда стало Total parse time 0.1874032 s. Но у гугла время ответа сервера от 210мс до 420мс. Как еще сократить время pdoPage?
      Павел Гвоздь
      06 августа 2018, 08:02
      0
      1) Переписать вывод самого сниппета и чанков, используемых им, на Феном.
      2) Вместо &includeThumb попробовать получать картинку непосредственно в чанке. И вообще, задаться вопросом, нужен ли этот дополнительный джоин (даже 2, помоему) в запросе.

      На самом деле вариантов множество, нужно тестировать.
Евгений Шеронов
06 августа 2018, 09:50
0
Зачем на главной вызов pdoPage, если нет пагинации?
Достаточно просто msProducts.
Инклудить большие превьюшки тоже не нужно, раз в Ajax модалька, пусть там у товара image используется
Если изначально thumb небольшого размера — то можно перегенерировать.
    Александр
    06 августа 2018, 10:11
    0
    Зачем на главной вызов pdoPage, если нет пагинации?
    Достаточно просто msProducts.
    Пагинация есть. Кнопка загрузить еще. Только она когда вызываешь сниппет кешируемым не показывается. Кстати, если бы не это, то можно было бы вызвать pdoPage кешируемым и Total parse time 0.1153460 s. В рамки гугл бы укладывался. Но вот не работает пагинация.
Yar
Yar
06 августа 2018, 12:31
0
А зачем вообще оптимизировать главную страницу? Трафик с поиска и рекламы, как правило, идет на страницы товаров, а там pdoPage можно вообще не использовать
    Алексей Соин
    06 августа 2018, 12:54
    0
    чтоб когда кидаешь ссылку заказчику он видел примерно такое


    как правило никто внутренние ссылки не проверяет через гугл)))
Himurovich
06 августа 2018, 16:00
1
+2
Хорошей вам инструкции и чтобы все получилось! Хотелось бы добавить одно — посоветовать оптимизировать и сверяться с результатами измерений нескольких утилит, так сказать, на пересечении множеств. Page Speed Insights по большому счету не измерительный, а рекомендательный инструмент, вооружитесь результатами, например, еще Gtmetrix и tools.pingdom.com. Бывает так, что у PSI все уже хорошо, а у других — пока еще нет. Или же наоборот. Посему полагаю, что нужен кворум :)
Александр
06 августа 2018, 22:59
+1
Нашел 2 ошибки с кешем в pdoPage
    Алексей
    10 августа 2018, 07:23
    0
    У кого-нибудь есть контакты Василия? Звякните ему чтоб ошибки в pdoPage поправил.

    Можно поправить прямо в исходниках репозитория pdoTools на гитхабе.
    Вот ссылка на файлик
    core/components/pdotools/model/pdotools/pdotools.class.php
    github.com/bezumkin/pdoTools/blob/master/core/components/pdotools/model/pdotools/pdotools.class.php#L1389
    ну и создать пулл-регвест. Вроде по самому короткому пути так.
    PS: кстати у меня открывается ваш сайт за ~400-500мсек.
      Александр
      12 августа 2018, 17:00
      0
      Сделал пулл-регвест. https://github.com/bezumkin/pdoTools/pull/278 описание правда заново не стал писать. Надеюсь Василий разберется.
      PS: кстати у меня открывается ваш сайт за ~400-500мсек.
      А у меня время ответа сайта прыгает от 183 до 700 мс. В основном 180-195мс а иногда 300 или 700мс. debugParser ничего такого не показывает. У него от 100мс до 180мс. Как выяснить из-за чего прыгает?
        Сергей Шлоков
        12 августа 2018, 17:55
        0
        Мда, вместо того, чтобы всего лишь поправить сниппет pdoPage (поднять setTotal на несколько строчек, а request удалить), нужно зачем-то было лезть в ядро. Зачем? Если нет достаточно опыта, то лучше написать issue.
          Александр
          12 августа 2018, 18:10
          0
          Ну я хотел сначала issue, но не разобрался где оно там на гитхабе, а с пулл-регвест разобрался. На pdoTools issue кажется вообще заблокированы. Это во первых. Во вторых setTotal мне вообще не понятно зачем нужно и зачем оно именно в том месте. А request лучше в ядре удалить. Из-за него у всех сниппетов pdoTools проблемы с кешированием.
            Сергей Шлоков
            12 августа 2018, 18:42
            0
            Т.е. я не знаю зачем это нужно, поэтому удалю нафиг? Ну может для начала прочитать про это.

            А request лучше в ядре удалить. Из-за него у всех сниппетов pdoTools проблемы с кешированием.
            В каких сниппетах он ещё используется?
              Александр
              12 августа 2018, 18:54
              0
              Т.е. я не знаю зачем это нужно, поэтому удалю нафиг?
              Да :). Там где это точно не на что не повлияет. Просто имя кеша будет другое и все. А в сам $scriptProperties не трогаю. Я ведь верно понимаю, что удаление из переменной $options внутри функции на $scriptProperties не влияет? И в остальном коде setTotal присутствует?
              Не знаю в каких. Наверно не в каких, но если вдруг где-то еще кеш используется, то очистка зависимости имени файла кеша от request не помешает.
              И вообще включите на гитхабе issue и я бы вообще в этот код не лез.
        Алексей
        14 августа 2018, 09:18
        0
        Спасибо за pull-request! Я уже давно заметил что pdoPage не кэшируется, но, думал что это из-за переменных &page=2 в строке запроса. Оказывается что нет, будем ждать новой версии pdoTools с кэширующим pdoPage!
        время ответа сайта прыгает от 183 до 700 мс.
        Это хостинг. Можно развернуть локальное окружение и попробовать на своём ПС. Но в 90% это виноват хостинг. Если локально не получается, попробуйте на тестовом тарифе modhost.pro развернуть сайт, там процессор E3-1270 v6 последнего поколения, скачков во времени ответа быть вообще не должно.
Никита Серов
08 августа 2018, 11:56
0
Хорошая статья, познавательная, ну лично для меня.
А вот такой вопрос, как быть с картинками, которые проходят через phpThumbOn?
Я к тому, что я вставляю картинки так:
<img class="img-responsive" src="[[+image:phpthumbon=`w=300&zc=1`]]" title="[[+pagetitle]]" alt="[[+pagetitle]]">
PageSpeed Insights для Mobile — пишет 100 баллов.
Для Desktop — пишет 84 и говорит, что мне нужно оптимизировать изображения. То есть они по факту получаются 300 на 300, а отображаются 282 на 282. Но в чем прикол? На мобильной версии, там еще меньше получается, то есть картинка-то 300х300, а уменьшается отображаемая картинка до 192х192.
Может кто подскажет, как это работает???
    Алексей Ерохин
    08 августа 2018, 12:09
    0
    picture
    или img с srcset и sizes
    responsiveimages.org/#implementation
      Никита Серов
      09 августа 2018, 12:48
      0
      А это что такое? принудительная подстановка определенного изображения под определенную ширину экрана, я просто бегло просмотрел, не вчитывался. Английский со словарем — мой уровень (наверное так, правильно сказать).
    Himurovich
    08 августа 2018, 12:19
    +1
    @Никита Серов они хотят, чтобы в идеале разрешение картинки совпадало с тем, как она будет масштабирована в итоге, это и есть идеальный вариант. Другое дело что для адаптивных сайтов этого не всегда легко добиться. Попробуйте поиграть в CSS с width height или в явно указать эти атрибуты. Иногда помогает.
      Никита Серов
      09 августа 2018, 12:47
      0
      Понятно, я так сразу и подумал, что если у меня блок для картинки размером 300 на 300, то и источник у картинки должен быть 300 на 300, не больше. Да, при адаптивной верстке сложно добиться. Только если всегда делать блоки одинакового размера, но разное количество в ряду. Жестко прописать… Надо подумать, спасибо за подсказку.
        Viktor
        21 августа 2018, 21:03
        0
        вот такое извращение есть, браузер будет подгружать нужную картинку в зависимости от разрешения экрана
        <picture> 
            <source srcset="image-big.jpg" media="(min-width: 768px)">
            <img src="image-small.jpg" alt=""> 
        </picture>