Решил задачу, рассказал об этом
Сегодня встала задача сократить время загрузки главной страницы сайта до 1 секунды и увеличить все показания от pegespeed до зеленой зоны. Сайт на битрикс.
Итак, как я справился с этой очень трудной задачей. Сразу скажу, что все стандартные методы которые описаны в интернете, к сожалению не работают. Битрикс очень нагруженная система разными модулями и компонентами и на странице подключает большое количество своих скриптов и стилей.
Такие способы, как описанные ниже сильной прибавки в скорости вам не сделает
1. Настройка кэширования
2.Оптимизация изображений
3.Минификация и объединение CSS и JS
4. Использование CDN
5. Использовать сжатие GZip
6. Располагать скрипты в конце документа, а стили в начале документа
7. И прочее и так далее.
Что действительно поможет, так это следующее действие
Идея в следующем. Сведение к одному HTTP-запросу. Мы разделим ее на два этапа. Сначала я расскажу на словах, чтобы понять саму механику и далее приведу код как можно это реализовать.
Это первый этап. Тут вытаскиваем из шаблона только необходимые, html, js, css, картинки, иконки, шрифты и повторяем структуру html блоков, Оптимизируем, переделываем, сокращаем все до минимума. Убираем все системные битриксовые запросы, метрики, статистики и прочее, что загрызается вначале
По сути разработка нового шаблона.
Это второй этап. Тут нужно провести анализ всех скриптов и стилей и шрифтов и изображений, оптимизировать, сократить, сжать и убрать ненужные, чтобы облегчить данные + подгружать только нужные скрипты и стили для страницы т.к. сейчас подгружаются одновременно ВСЕ скрипты и стили и шрифты для сайта, даже которые не нужны.
Также оптимизация затронет фронтнд, т.е. установлено много сторонних модулей, наших компонентов и виджетов, которые устанавливают на странице свои скрипты и стили, нужно попробовать все переделать на отложенную загрузку, проанализировать подключаемые библиотеки картинки, разделить и прочее, может они вовсе не нужны.
Первое что было сделано, это приведение в порядок функционала на стороне бэкэнда, чтобы страница отдавалась в браузер быстро. Для этого достаточно сделать отладку стандартным функционалом битрикс и посмотреть какие компоненты и функции тратят много времени и запросов к базе данных и допилить их. По большей части это грамотно настроить кэширование. Также посмотреть где хранится кэш, если на файлах, то перевести его на mamcached. Тут нужно чтобы был установлен php модуль mamcached. И добавить некоторые данные в файл .settings.php, которые можно найти в интернете Нам нужно добиться чтобы страница отдавалась быстро по метрикам до 300 мс
Второе мы делаем отдельный легкий шаблон, который будет состоять из шапки, подвала и картинкой главного баннера
Третье, мы пишем самые необходимые для отображения первого экрана стили и скрипты прямо в документе, в header шаблона конкретной главной страницы, а не всего сайта целиком и в нескольких файлах. А отдельные файлы стилей и скриптов отправляем после загрузки страницы, когда пользователь уже начал взаимодействие с сайтом
В итоге после этих этапом мы отдаем браузеру один файл в котором есть структура документа встроенные стили и скрипты, для начала взаимодействия пользователя с сайтом. Остальной контент, стили и скрипты и картинки мы подгружаем по мере взаимодействия пользователя с сайтом. Сайт загружается практически за пол секунды
Четвертый пункт заключается в том мы вешаем на события scroll, ready click ajax запросы, для получения данных основного котента станицы и их стилей, скриптов и картинок .
Первое, у нас на главной помимо шапки и подвала, может быть верхняя и нижняя секция. Например верхняя это слайдер из больших баннеров а нижняя лента с новостями.
При первоначальной загрузки сайта у нас стоит просто блок в котором одно первое статичное изображение баннера. Сейчас мы вешаем на событие ready полную загрузку слайдера. Для этого в Легком шаблоне у нас должен быть прописан код, из которого явно читается что мы отдаем первый статичный слайд, и когда прилетает ajax запрос мы отдаем компонент.
<?if (strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest' && isset($_REQUEST['getTop'])){?>
<?
$APPLICATION->IncludeComponent(
"bitrix:main.include",
"",
Array(
"AREA_FILE_SHOW" => "page",
"AREA_FILE_SUFFIX" => "top",
"EDIT_TEMPLATE" => "standard.php"
)
);
?>
<?}else{?>
<div class="index_top_wrapper">
<div id="elem" class="index_top" style="">
<div class="index_top_plug" style="
background-image: url(/local/templates/light/images/first_slide.jpg);">
</div>
</div>
</div>
<?}?>
Событие на js
$(document).ready(function() {
getTop();
});
function getTop(){
var z = $(".index_top_wrapper");
if (!z.data("firstInit_index_top_wrapper")) {
z.data("firstInit_index_top_wrapper", !0);
$.ajax({
url: "/light/index.php?getTop",
dataType: "html",
success: function (data) {
$('.index_top').html($('<div>'+data+'</div>').find('.index_top').html()).each(function() {
function add(){
initMainBanner();
}
StyleBlack = $('<div>'+data+'</div>').find('script[data=StyleBlack]').html();
if(StyleBlack != undefined){
const obj_StyleBlack = JSON.parse(StyleBlack);
loadStyle(obj_StyleBlack ,0);
}
jsScriptBlack = $('<div>'+data+'</div>').find('script[data=ScriptBlack]').html();
if(jsScriptBlack != undefined){
const obj = JSON.parse(jsScriptBlack);
load(obj ,0, add);
}
});
}
});
}
}
Также мы делаем для нижний секции, но вешаем событие уже на scroll. Т.к. этот блок находится внизу страницы и когда пользователь доберется но него он уже будет готов. Еще можно навесить функционал отслеживание появление блока в зоне видимости. И только в этот момент отправлять запрос на получение данных.
<?if (strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest' && isset($_REQUEST['getBottom'])){?>
<?
$APPLICATION->IncludeComponent(
"bitrix:main.include",
"",
Array(
"AREA_FILE_SHOW" => "page",
"AREA_FILE_SUFFIX" => "bottom",
"EDIT_TEMPLATE" => "standard.php"
)
);
?>
<?}else{?>
<div class="index_bottom_wrapper">
<div class="index_bottom"></div>
</div>
<?}?>
Событие на js
$(document).scroll(function() {
getBottom();
});
function getBottom(){
var z = $(".index_top_wrapper");
if (!z.data("firstInit_index_bottom_wrapper")) {
z.data("firstInit_index_bottom_wrapper", !0);
$.ajax({
url: "/light/index.php?getBottom",
dataType: "html",
success: function (data) {
$('.index_bottom').html($('<div>'+data+'</div>').find('.index_bottom').html()).each(function() {
StyleBlack = $('<div>'+data+'</div>').find('script[data=StyleBlack]').html();
if(StyleBlack != undefined){
const obj_StyleBlack = JSON.parse(StyleBlack);
loadStyle(obj_StyleBlack ,0);
}
jsScriptBlack = $('<div>'+data+'</div>').find('script[data=ScriptBlack]').html();
if(jsScriptBlack != undefined){
const obj = JSON.parse(jsScriptBlack);
function add(){
initSlider();
}
load(obj ,0, add);
}
});
}
});
}
};
Второе нам нужно получить основные контент блоки для страницы. Эти события мы также вешаем на событие скрол по тому же принцыпу что описано выше.
Третье что необходимо учесть это функционал кликов по кнопкам, например авторизации или выбора города. Тут мы вешаем события на click. И получаем данные в момент клика пользователя по нужной кнопке. Т.к. таких кнопок может быть много, мы создаем отдельный файл со списком компонентов и получаем эти компоненты в зависимости от параметра CODE
$('#check_location').click(function(e){
e.preventDefault();
get_check_location();
});
function get_check_location(){
$.ajax({
url: "/light/index_c.php?action=sale_location_selector_search&CODE=<?=$lcode?>",
dataType: "html",
success: function (data) {
$('.location_selector').html($('<div>'+data+'</div>').find('.location_selector').html()).each(function() {
StyleBlack = $('<div>'+data+'</div>').find('script[data=StyleBlack]').html();
if(StyleBlack != undefined){
const obj_StyleBlack = JSON.parse(StyleBlack);
loadStyle(obj_StyleBlack ,0);
}
jsScriptBlack = $('<div>'+data+'</div>').find('script[data=ScriptBlack]').html();
if(jsScriptBlack != undefined){
const obj = JSON.parse(jsScriptBlack);
function add(){
addAnswer.show();
}
load(obj ,0, add);
}
});
}
});
}
На всех примерах выше мы получаем методом ajax всю страницу целиком, вытаскиваем блоки которые нам нужно и вставляем на старание. Стили и скрипты также лежат в спец блоках, мы их получаем, парсим в объект и далее загружаем функцией load на страницу. Функция add() - это коллбэк функция и отрабатываем после загрузки скриптов. Теперь мы подошли к самому главному, а именно
Как было видно из кода представленного ранее мы делаем запрос ко всей странице на сервере и вытаскиваем из нее нужный нам контент, стили и скрипты. Чтобы этого добиться мною был разработан специальный функционал, приведенный ниже. Мы отслеживаем событие на битрикс OnEndBufferContent модуля main и запускаем функцию modify
AddEventHandler("main", "OnEndBufferContent", "modify");
Суть заключатся в том, что мы парим страницу регулярными выражениями и вытаскиваем стили и скрипты которым в последствии устанавливаем индивидуальный mid. Удаляем их со страницы для прямого подключения, а пишем их в специальный блок, откуда достаем в последствии в рамках ajax ответа и вставляем на странице проверяя уникальность mid. Этот параметр проверяет уникальность скрипта. Все это дополнено скриптовыми колбэк функциями, которые запускаются после отработки вставки скриптов и стилей.
function modify(&$content) {
global $USER, $APPLICATION;
if(CSite::InDir('/light/')||(strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) != 'xmlhttprequest' && CSite::InDir('/light/'))){}else return;
if($APPLICATION->GetProperty("save_kernel") == "Y") return;
$arPatternsToRemove = '/(<script(.*?)>(.*?)<\/script\>)/s';
$arPatternsToRemoveLink = '/(<link.*?\>)/s';
$jsStyleBlack = array();
preg_match_all($arPatternsToRemove, $content, $matches);
preg_match_all($arPatternsToRemoveLink, $content, $matchesLink);
$content = preg_replace($arPatternsToRemove, "", $content);
$content = preg_replace($arPatternsToRemoveLink, "", $content);
$jsScriptBlack = array();
if(count($matchesLink)){
$in = 0;
foreach($matchesLink[1] as $index => $value){
preg_match('/href=["\'](.*?)["\']/s', $value, $matchess);
if(strpos($value, 'stylesheet')){
$jsStyleBlack[$in]['href'] = $matchess[1];
$jsStyleBlack[$in]['md5'] = md5(preg_replace('/\s+/','',$value));
$jsStyleBlack[$in]['rel'] = 'stylesheet';
$in++;
}
if(strpos($value, 'manifest')){
$jsStyleBlack[$in]['href'] = $matchess[1];
$jsStyleBlack[$in]['md5'] = md5(preg_replace('/\s+/','',$value));
$jsStyleBlack[$in]['rel'] = 'manifest';
$in++;
}
}
}
foreach($matches[1] as $index => $value){
$matches[4][$index]['md5'] = md5(preg_replace('/\s+/','',$value));
}
foreach($matches[2] as $index => $value){
preg_match('/src=["\'](.*?)["\']/s', $value, $matchesss);
$matches[2][$index] = $matchesss[1];
preg_match('/data-template-id=["\'](.*?)["\']/s', $value, $data_template_id);
$matches[4][$index]['data_template_id'] = $data_template_id[1];
preg_match('/data-skip-moving=["\'](.*?)["\']/s', $value, $data_skip_moving);
$matches[4][$index]['data_skip_moving'] = $data_skip_moving[1];
preg_match('/class=["\'](.*?)["\']/s', $value, $class);
$matches[4][$index]['class'] = $class[1];
}
foreach($matches[2] as $index => $value){
if($value){
$jsScriptBlack[$index]['src'] = $value;
}
if($matches[3][$index]){
$jsScriptBlack[$index]['text'] = $matches[3][$index];
}
if($matches[4][$index]['data_template_id']){
$jsScriptBlack[$index]['data_template_id'] = $matches[4][$index]['data_template_id'];
}
if($matches[4][$index]['data_skip_moving']){
$jsScriptBlack[$index]['data_skip_moving'] = $matches[4][$index]['data_skip_moving'];
}
if($matches[4][$index]['class']){
$jsScriptBlack[$index]['class'] = $matches[4][$index]['class'];
}
if($matches[4][$index]['md5']){
$jsScriptBlack[$index]['md5'] = $matches[4][$index]['md5'];
}
}
if(count($jsScriptBlack)>0 ){
$JsBlack = "
<script data='ScriptBlack' type='text/javascript'>".json_encode( $jsScriptBlack )."</script>
<script data='StyleBlack' type='text/javascript'>".json_encode( $jsStyleBlack )."</script>
<script type='text/javascript'>
window.jsScriptBlack = ".json_encode( $jsScriptBlack ).";
window.jsStyleBlack = ".json_encode( $jsStyleBlack ).";
function loadStyle(jsStyleBlack, key){
// console.log(jsStyleBlack);
if (jsStyleBlack.hasOwnProperty(key)) {
const value = jsStyleBlack[key]['href'];
const mid = jsStyleBlack[key]['md5'];
const rel = jsStyleBlack[key]['rel'];
// console.log(jsStyleBlack[key]);
if(document.querySelectorAll('[mid=\"'+mid+'\"]').length == 0){
const link = document.createElement('link');
link.setAttribute(
'type',
'text/css',
);
link.setAttribute(
'rel',
rel,
);
link.setAttribute(
'href',
value,
);
link.setAttribute(
'mid',
mid,
);
if(rel == 'manifest'){
loadStyle(jsStyleBlack ,key*1 + 1);
}else{
link.onload = function handleStyleLoaded() {
// console.log(jsStyleBlack);
loadStyle(jsStyleBlack ,key*1 + 1);
};
}
document.body.appendChild(link);
// console.log(link);
}else{
loadStyle(jsStyleBlack ,key*1 + 1);
}
}
}
function load(jsScriptBlack, key, execFunc = (func) => {}, params = {}){
if (jsScriptBlack.hasOwnProperty(key)) {
var value = jsScriptBlack[key];
if(document.querySelectorAll('[mid=\"'+value.md5+'\"]').length == 0){
if(value.text){
const script = document.createElement('script');
script.setAttribute(
'type',
'text/javascript',
);
if(value.class){
script.setAttribute(
'class',
value.class,
);
}
if(value.md5){
script.setAttribute(
'mid',
value.md5,
);
}
if(value.data_template_id){
script.setAttribute(
'data-template-id',
value.data_template_id,
);
script.setAttribute(
'type',
'text/html',
);
}
if(value.data_skip_moving){
script.setAttribute(
'data-skip-moving',
value.data_skip_moving,
);
}
script.text = value.text;
document.body.appendChild(script);
// console.log(script);
load(jsScriptBlack ,key*1 + 1,execFunc);
}
if(value.src){
const script = document.createElement('script');
script.setAttribute(
'type',
'text/javascript',
);
script.setAttribute(
'src',
value.src,
);
if(value.class){
script.setAttribute(
'class',
value.class,
);
}
if(value.data_template_id){
script.setAttribute(
'data-template-id',
value.data_template_id,
);
script.setAttribute(
'type',
'text/html',
);
}
if(value.md5){
script.setAttribute(
'mid',
value.md5,
);
}
if(value.data_skip_moving){
script.setAttribute(
'data-skip-moving',
value.data_skip_moving,
);
}
script.onload = function handleScriptLoaded() {
load(jsScriptBlack ,key*1 + 1,execFunc);
};
document.body.appendChild(script);
// console.log(script);
}
document.dispatchEvent(new CustomEvent('loadJsScriptBlack', {
detail: { md5: value.md5 , load: params.load}
}));
} else{
load(jsScriptBlack ,key*1 + 1,execFunc);
}
}else{
execFunc();
document.dispatchEvent(new CustomEvent('loadJsScriptBlackEnd', {
detail: { event: 0 , load: params.load}
}));
}
}
window.addEventListener('load', function(event) {
loadStyle(window.jsStyleBlack, 0);
load(window.jsScriptBlack ,0, function(){}, {load:1});
})
</script>
";
$JsBlack .= "
</body>
";
$content = str_replace("</body>", $JsBlack, $content);
}
}
В итоге мы пришли к моментальной первоначальной загрузки страницы, а все http запросы отложили по мере взаимодействия пользователя с сайтом. Сайт загружается менее секунды и google pagespaeed показывает полностью зеленую производительность.