This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
function escapeHtml(value) { return String(value).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", '''); }
|
||||
const p = (value) => '<p>' + value + '</p>';
|
||||
const h2 = (value) => '<h2>' + value + '</h2>';
|
||||
const code = (value) => '<pre><code>' + escapeHtml(Array.isArray(value) ? value.join('\n') : value) + '</code></pre>';
|
||||
const figure = (src, alt, caption) => '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
|
||||
const ol = (items) => '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
|
||||
const table = (caption, headers, rows) => '<div class="table-scroll"><table><caption>' + caption + '</caption><thead><tr>' + headers.map((item) => '<th scope="col">' + item + '</th>').join('') + '</tr></thead><tbody>' + rows.map((row) => '<tr>' + row.map((item) => '<td>' + item + '</td>').join('') + '</tr>').join('') + '</tbody></table></div>';
|
||||
const sources = (items) => '<ul>' + items.map((item) => '<li><a href="' + item.url + '" target="_blank" rel="noopener noreferrer">' + item.title + '</a> — ' + item.note + '</li>').join('') + '</ul>';
|
||||
|
||||
function plainText(html) { return html.replace(/<[^>]+>/g, ' ').replace(/&(?:quot|amp|lt|gt|#039);/g, ' ').replace(/\s+/g, ' ').trim(); }
|
||||
function bodyText(html) { return plainText(html.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, '')); }
|
||||
function revision(meta, chunks, referenceList) {
|
||||
const contentHtml = chunks.join('\n') + '\n' + h2('Проверяемые источники') + sources(referenceList);
|
||||
const length = bodyText(contentHtml).length;
|
||||
if (length < 5000 || length > 15000) throw new Error(meta.slug + ': основной текст вне диапазона: ' + length);
|
||||
return { ...meta, contentHtml, proseLength: length };
|
||||
}
|
||||
|
||||
const startResultCache = {
|
||||
title: '1С-Битрикс: CBitrixComponent::StartResultCache, архивный снимок 27 сентября 2022 года',
|
||||
url: 'https://web.archive.org/web/20220927080127id_/https://dev.1c-bitrix.ru/api_help/main/reference/cbitrixcomponent/startresultcache.php',
|
||||
note: 'официальная документация Bitrix, доступная до ноября 2022 года. Описывает возврат false при действительном кеше, true при недействительном, а также базовые части зависимости: SITE_ID, компонент, шаблон и $arParams.',
|
||||
};
|
||||
const resultCacheKeys = {
|
||||
title: '1С-Битрикс: CBitrixComponent::SetResultCacheKeys, архивный снимок 15 августа 2022 года',
|
||||
url: 'https://web.archive.org/web/20220815073221id_/https://dev.1c-bitrix.ru/api_help/main/reference/cbitrixcomponent/setresultcachekeys.php',
|
||||
note: 'официальная документация Bitrix, доступная до ноября 2022 года. Описывает список частей $arResult, которые сохраняются при встроенном кешировании; не задаёт производительность конкретной страницы.',
|
||||
};
|
||||
const bitrixCachingCourse = {
|
||||
title: '1С-Битрикс: Кеширование, архивный снимок 29 июня 2022 года',
|
||||
url: 'https://web.archive.org/web/20220629225339id_/https://dev.1c-bitrix.ru/learning/course/?COURSE_ID=43&LESSON_ID=3485',
|
||||
note: 'официальный учебный материал Bitrix в историческом snapshot. Он различает компонентное, неуправляемое и управляемое кеширование; не является trace, профилировщиком или рекомендацией для неизвестного проекта.',
|
||||
};
|
||||
const commonSources = [startResultCache, resultCacheKeys, bitrixCachingCourse];
|
||||
|
||||
/**
|
||||
* Учебная детерминированная модель контракта evidence.
|
||||
* Здесь нет Bitrix runtime, PHP, HTTP, SQL, файлового кеша, HTML renderer,
|
||||
* профилировщика, часов, миллисекунд, метрик, trace или реального cache hit.
|
||||
* Все значения передаются fixture как объявленные свойства учебного запроса.
|
||||
*/
|
||||
function createTeachingRequest(input) {
|
||||
return Object.freeze({
|
||||
boundary: Object.freeze({ kind: 'teaching-bitrix-page-evidence-v1', bitrixRuntime: 'not-started', php: 'not-executed', http: 'not-sent', sql: 'not-performed', cacheStorage: 'not-opened', template: 'not-rendered', duration: 'not-collected', profiler: 'not-connected' }),
|
||||
route: input.route,
|
||||
component: input.component,
|
||||
template: input.template,
|
||||
cacheMode: input.cacheMode,
|
||||
declaredKeyParts: Object.freeze([...input.declaredKeyParts]),
|
||||
requiredKeyParts: Object.freeze([...input.requiredKeyParts]),
|
||||
rollbackTarget: input.rollbackTarget,
|
||||
});
|
||||
}
|
||||
function missingKeyParts(model) { return model.requiredKeyParts.filter((part) => !model.declaredKeyParts.includes(part)); }
|
||||
function inspectTeachingRequest(model) {
|
||||
const missing = missingKeyParts(model);
|
||||
const branch = missing.length ? 'stop-incomplete-cache-contract' : model.cacheMode === 'declared-reuse' ? 'declared-cache-reuse' : 'declared-cache-build';
|
||||
return Object.freeze({
|
||||
kind: 'teaching-page-evidence',
|
||||
route: model.route,
|
||||
component: model.component,
|
||||
template: model.template,
|
||||
cacheMode: model.cacheMode,
|
||||
branch,
|
||||
missingKeyParts: Object.freeze(missing),
|
||||
evidence: Object.freeze({ route: 'declared', component: 'declared', template: 'declared', cacheInputs: missing.length ? 'incomplete' : 'complete', duration: model.boundary.duration, sql: model.boundary.sql, profiler: model.boundary.profiler }),
|
||||
nextAction: missing.length ? 'name-missing-cache-input-before-changing-cache' : 'collect-one-real-artifact-at-the-named-boundary',
|
||||
rollback: model.rollbackTarget,
|
||||
notClaim: 'not-a-bitrix-request-or-performance-measurement',
|
||||
});
|
||||
}
|
||||
function applyTeachingChange(model, change) {
|
||||
if (change.kind !== 'replace-template-owner') return Object.freeze({ kind: 'change-rejected', changed: false, reason: 'unsupported-teaching-change' });
|
||||
const before = inspectTeachingRequest(model);
|
||||
if (before.branch === 'stop-incomplete-cache-contract') return Object.freeze({ kind: 'change-blocked', changed: false, reason: 'incomplete-cache-contract', rollback: model.rollbackTarget });
|
||||
const next = createTeachingRequest({ ...model, template: change.template, rollbackTarget: model.template });
|
||||
return Object.freeze({ kind: 'teaching-change-planned', changed: true, before, after: inspectTeachingRequest(next), changedModel: next, rollback: model.template });
|
||||
}
|
||||
function rollbackTeachingChange(model, targetTemplate) {
|
||||
const restored = createTeachingRequest({ ...model, template: targetTemplate, rollbackTarget: model.template });
|
||||
return Object.freeze({ kind: 'rollback-planned', changed: true, report: inspectTeachingRequest(restored) });
|
||||
}
|
||||
|
||||
export function runBitrixPerformanceFixture() {
|
||||
const complete = createTeachingRequest({ route: '/catalog/', component: 'catalog.section', template: 'catalog-grid', cacheMode: 'declared-reuse', declaredKeyParts: ['SITE_ID', 'component', 'template', 'arParams', 'visitor-segment'], requiredKeyParts: ['SITE_ID', 'component', 'template', 'arParams', 'visitor-segment'], rollbackTarget: 'catalog-grid' });
|
||||
const incomplete = createTeachingRequest({ route: '/catalog/', component: 'catalog.section', template: 'catalog-grid', cacheMode: 'declared-build', declaredKeyParts: ['SITE_ID', 'component', 'template', 'arParams'], requiredKeyParts: ['SITE_ID', 'component', 'template', 'arParams', 'visitor-segment'], rollbackTarget: 'catalog-grid' });
|
||||
const completeReport = inspectTeachingRequest(complete);
|
||||
const incompleteReport = inspectTeachingRequest(incomplete);
|
||||
const planned = applyTeachingChange(complete, { kind: 'replace-template-owner', template: 'catalog-grid-minimal' });
|
||||
const blocked = applyTeachingChange(incomplete, { kind: 'replace-template-owner', template: 'catalog-grid-minimal' });
|
||||
const rollback = rollbackTeachingChange(planned.changedModel, planned.rollback);
|
||||
return Object.freeze({ completeReport, incompleteReport, planned, blocked, rollback, assertions: Object.freeze({
|
||||
noBitrixRuntime: complete.boundary.bitrixRuntime === 'not-started', noSql: complete.boundary.sql === 'not-performed', noDuration: completeReport.evidence.duration === 'not-collected', noProfiler: complete.boundary.profiler === 'not-connected',
|
||||
completeContractRecognized: completeReport.evidence.cacheInputs === 'complete', declaredReuseIsNotActualHit: completeReport.branch === 'declared-cache-reuse' && completeReport.notClaim === 'not-a-bitrix-request-or-performance-measurement',
|
||||
incompleteContractStops: incompleteReport.branch === 'stop-incomplete-cache-contract', missingSegmentNamed: incompleteReport.missingKeyParts[0] === 'visitor-segment', blockedChangeDoesNotMutate: blocked.changed === false && blocked.reason === 'incomplete-cache-contract',
|
||||
plannedChangeNamesRollback: planned.changed === true && planned.rollback === 'catalog-grid', plannedChangeSwapsTeachingTemplate: planned.after.template === 'catalog-grid-minimal',
|
||||
rollbackRestoresDeclaredTemplate: rollback.report.template === 'catalog-grid' && rollback.report.template !== planned.after.template, rollbackStillHasNoMeasurements: rollback.report.evidence.duration === 'not-collected',
|
||||
}) });
|
||||
}
|
||||
|
||||
const fixtureExample = [
|
||||
"import { runBitrixPerformanceFixture } from './upgrade-2022-11.mjs';",
|
||||
'const report = runBitrixPerformanceFixture();',
|
||||
'if (!Object.values(report.assertions).every(Boolean)) throw new Error(\'fixture contract failed\');',
|
||||
'console.log(report.incompleteReport.nextAction);',
|
||||
"// name-missing-cache-input-before-changing-cache",
|
||||
'console.log(report.planned.rollback);',
|
||||
"// catalog-grid",
|
||||
];
|
||||
|
||||
const practice = revision({ slug: 'editorial-2022-11-practice-bitrix-performance', title: 'Производительность Bitrix-страницы: минимальная инженерная схема', categories: ['Bitrix', 'Производительность'], cover: '/assets/editorial/2022/bitrix-performance-2022-request-contract.svg', excerpt: 'Как не назвать Bitrix причиной раньше evidence: разделить запрос, компонент, кеш и шаблон, собрать контракт фактов и выбрать обратимое действие.', readingMinutes: 11 }, [
|
||||
p('Медленную Bitrix-страницу часто объясняют одним словом: «Bitrix». В нём исчезают маршрут, компонент, вариант шаблона и настройка кеша. Цена такой формулировки — не только спор в чате. Можно отключить кеш на всей странице, переписать шаблон вслепую или поднять ресурсы, а исходный симптом останется на месте. Следующему человеку достанется больше кода и ни одного факта о том, где искать дальше.'),
|
||||
p('Начинаем не с оптимизации, а с маленького контракта evidence. Для одного URL записываем: какой маршрут открыт, какой компонент рассматриваем, какой шаблон объявлен, какие входы влияют на HTML, какую ветку кеша мы ожидаем проверить и куда откатывается один небольшой diff. Это не SQL trace и не профиль. Но такой список не позволяет выдать общую гипотезу за причину.'),
|
||||
h2('Четыре слоя вместо одного виновника'),
|
||||
p('У страницы есть как минимум четыре инженерные границы: вход запроса, компонент, решение о встроенном кеше и шаблон, который формирует HTML. Они идут рядом, но не взаимозаменяемы. Ошибка в параметрах компонента не доказывает тяжёлый шаблон. Объявленный режим кеша не доказывает, что на конкретном хите был cache hit. А большой HTML не позволяет без отдельного наблюдения назвать источник данных или SQL-запрос.'),
|
||||
p('Документация Bitrix для `StartResultCache` задаёт полезную, но узкую рамку: метод возвращает false при действительном кеше и true, когда результат нужно сформировать. Она также перечисляет базовые части зависимости кеша: сайт, компонент, шаблон и `$arParams`; дополнительные условия должны быть переданы отдельно. Это знание о контракте компонента, не измерение страницы. Поэтому в заметке слова «reuse» и «build» остаются учебными ветками, а не отчётом живого сервера.'),
|
||||
table('Что именно нужно разделить до правки', ['Слой', 'Какой факт записать', 'Что этот факт не доказывает', 'Первое действие'], [
|
||||
['Маршрут', 'URL и вариант входа', 'какой PHP-код или SQL исполнился', 'повторить один и тот же вход'],
|
||||
['Компонент', 'имя и параметры, которые разбираются', 'что он единственный источник задержки', 'назвать владельца компонента'],
|
||||
['Кеш', 'режим и полный список входов ключа', 'реальный hit-rate или чтение файла', 'проверить неполный ключ'],
|
||||
['Шаблон', 'имя шаблона и его output contract', 'стоимость рендера в миллисекундах', 'собрать один артефакт именно на этой границе'],
|
||||
]),
|
||||
h2('Сначала сохраняем evidence, потом меняем код'),
|
||||
p('Заведите короткую карточку без оценок: `route=/catalog/`, `component=catalog.section`, `template=catalog-grid`, `cache inputs=SITE_ID, component, template, arParams, visitor-segment`, `rollback=catalog-grid`. Если один из этих пунктов неизвестен, так и пишем «неизвестен». Особенно важен вариант посетителя или другой внешний признак, если он меняет HTML. Его нельзя тихо спрятать в фразу «для пользователя всё по-другому»: это либо часть ключа, либо причина остановить изменение до уточнения.'),
|
||||
p('После карточки выбирается один вопрос. Например: «входит ли visitor-segment в объявленный ключ?» Он лучше вопроса «почему Bitrix медленный», потому что на него есть конечный ответ. Если входа нет, не меняем TTL, не очищаем все кеши и не объявляем шаблон виновным. Сначала уточняем контракт. Если вход назван, следующий факт выбирается на уже обозначенной границе: исходный код компонента, шаблон или разрешённый инструмент проекта.'),
|
||||
figure('/assets/editorial/2022/bitrix-performance-2022-request-contract.svg', 'Учебная схема разбора Bitrix-страницы: вход маршрута, компонент, решение о кеше, шаблон и HTML. Для каждого этапа сохранён объявленный факт, а реальные PHP, SQL, кеш и длительность прямо исключены из модели.', 'Это карта evidence для обсуждения и планирования. Она не является профилировщиком, сетевым следом или измерением времени ответа.'),
|
||||
h2('Проверяемый пример: модель не притворяется страницей'),
|
||||
p('Ниже запускается только fixture этого пакета. Она создаёт два JavaScript-объекта: один с полным набором объявленных частей ключа, другой без `visitor-segment`. Никакого Bitrix runtime, HTTP, PHP, файлового кеша, SQL, HTML или времени внутри нет. Поэтому выход `declared-cache-reuse` означает ровно «так названа ветка учебной модели», а не «сервер нашёл кеш».'),
|
||||
code(fixtureExample),
|
||||
p('Первый объект создаёт отдельную учебную версию с новым template owner и сохраняет `catalog-grid` как rollback. Второй останавливает изменение с `name-missing-cache-input-before-changing-cache`. Это сознательно полезнее фальшивого успеха: в реальном проекте именно недостающий вход может делать безопасную на вид правку неверной для части HTML. Fixture проверяет договорённость о следующем действии, а не платформу.'),
|
||||
h2('Маршрут: симптом → причина → проверка → действие'),
|
||||
ol([
|
||||
'<strong>Симптом.</strong> Зафиксируйте один маршрут и один видимый признак без объяснения причины: «на /catalog/ нужно исследование», а не «Bitrix долго отвечает».',
|
||||
'<strong>Причина гипотезы.</strong> Разложите её на request, component, cache и template. Нельзя одним наблюдением подтвердить сразу четыре слоя.',
|
||||
'<strong>Проверка контракта.</strong> Назовите компонент, шаблон, параметры и внешние условия, влияющие на HTML. Для встроенного кеша сверяйте их с API-контрактом Bitrix.',
|
||||
'<strong>Один артефакт.</strong> Выберите разрешённый исходник, лог или профиль, который существует в проекте. Не создавайте фиктивный SQL trace из догадки.',
|
||||
'<strong>Малое действие.</strong> Меняйте один владеемый параметр или шаблон только после достаточного evidence и заранее названного rollback.',
|
||||
'<strong>Повтор.</strong> Снова соберите тот же контракт. Если маршрут или вариант изменились, это новый случай, а не результат прежней правки.',
|
||||
]),
|
||||
h2('Кеш — не переключатель скорости'),
|
||||
p('В Bitrix есть компонентное, неуправляемое и управляемое кеширование; это разные механизмы с разными условиями обновления. Поэтому совет «включите кеш» слишком широк. Встроенный компонентный кеш может быть важен для выбранного блока, но не заменяет проверку шаблона, входных параметров и актуальности результата. Если продукт требует разный HTML для разных условий, цена неполного ключа выше, чем цена дополнительного вопроса на ревью.'),
|
||||
p('Есть и обратная ошибка: увидев вероятность устаревших данных, разработчик выключает кеш полностью. Это меняет сразу несколько переменных и делает следующий разбор хуже. Правильнее зафиксировать, какая зависимость должна участвовать в ключе, кто очищает или обновляет результат и какой пользовательский output допустим. Только после этого обсуждать срок жизни, tagged cache или отказ от кеша на конкретной границе.'),
|
||||
h2('Rollback должен быть частью первого diff'),
|
||||
p('Обратимость здесь практична. Если вы меняете шаблон компонента, заранее сохраните прежнее имя, владельца и условие повторной проверки. Откат — это не «вернуть всё как было после релиза», а конкретный маленький diff: восстановить прежний шаблон или параметр и повторить тот же вход. Не смешивайте с rollback очистку всего кеша: это разрушает контекст и может скрыть дефект вместо его объяснения.'),
|
||||
p('Если evidence неполный, тоже есть безопасный rollback: не выполнять оптимизацию. Остановка не означает, что проблемы нет. Она означает, что материал пока не отделяет шаблон от компонента или контракт ключа от наблюдения страницы. Следующий шаг — не новая настройка, а сбор недостающего названного факта с владельцем и способом воспроизведения.'),
|
||||
h2('Границы и следующий шаг'),
|
||||
p('Материал не утверждает, что конкретная страница стала быстрее, что Bitrix выполняет SQL в определённой последовательности или что у проекта есть cache hit-rate. В нём нет реального URL, пользователя, запроса, профиля, срока, скриншота и результата. Архивные документы Bitrix подтверждают только описанные API и кешевые зависимости, доступные до ноября 2022 года.'),
|
||||
p('Следующий рабочий шаг — выбрать одну страницу и заполнить карточку evidence из шести полей. На ревью спросить не «что тормозит?», а «какой слой и какой факт это различит?». Если ответ появляется, планируется одно обратимое изменение. Если нет — работа продолжается сбором материала, а не очередной оптимизацией наугад.'),
|
||||
h2('Историческая граница ноября 2022'),
|
||||
p('Для исторической части использованы только архивные снимки официальной документации 1С-Битрикс: август и сентябрь 2022 для API компонента и июнь 2022 для курса о кешировании. Текущие страницы платформы сознательно не используются как свидетельство состояния ноября 2022. Учебная fixture поверх документов остаётся собственной локальной моделью.'),
|
||||
], commonSources);
|
||||
|
||||
const mechanism = revision({ slug: 'editorial-2022-11-mechanism-bitrix-performance', title: 'Производительность Bitrix-страницы: модель, ограничения и границы', categories: ['Bitrix', 'Производительность'], cover: '/assets/editorial/2022/bitrix-performance-2022-cache-contract.svg', excerpt: 'Разбираем контракт встроенного кеша Bitrix: какие части ключа названы, где заканчивается документация API и почему model branch не равен наблюдению сервера.', readingMinutes: 12 }, [
|
||||
p('Фраза «компонент закеширован, значит страница быстрая» ломается сразу в двух местах. Она смешивает контракт компонента с итогом всего запроса и объявляет реальную производительность без наблюдения. Цена — опасные решения: в ключ не попадает условие, влияющее на HTML, шаблон перестают проверять, а любой следующий симптом объясняют кешем. В итоге команда теряет корректность и не получает диагностику.'),
|
||||
p('Ниже мы рассматриваем встроенный кеш Bitrix как договор о зависимости результата, а не как кнопку ускорения. Вопрос модели простой: перечислены ли входы, от которых зависит выдаваемый HTML? Если перечислены — можно планировать узкую проверку ветки компонента. Если нет — изменение останавливается. Учебная модель намеренно не говорит, читался ли файл кеша, сколько работал PHP или что происходило с базой данных.'),
|
||||
h2('Что действительно говорит API компонента'),
|
||||
p('В архивной документации `CBitrixComponent::StartResultCache` второй параметр описан как `additionalCacheID`. Базовая зависимость включает `SITE_ID`, имя компонента, имя шаблона и входные `$arParams`; дополнительное условие надо передать отдельно. В том же контракте true означает необходимость сформировать результат, false — использование действительного кеша. Этот API-факт достаточно точен, чтобы обсудить ключ. Он не даёт права утверждать, что конкретный HTTP-запрос прошёл одну из веток.'),
|
||||
p('`SetResultCacheKeys` решает другой вопрос: какие части `$arResult` нужны при встроенном кешировании. Его нельзя использовать как доказательство, что любой `$arResult` лёгкий или что шаблон ничего не делает. Сначала надо назвать, какой компонент и какой результат разбираются. Потом — посмотреть, совпадают ли состояния, важные для HTML, с теми, что участвуют в контракте. Только тогда разговор о размере, обновлении или структуре результата становится техническим, а не ритуальным.'),
|
||||
table('Три похожих вопроса, которые нельзя склеивать', ['Вопрос', 'Допустимый evidence', 'Недопустимый вывод', 'Действие'], [
|
||||
['Как устроен базовый ключ?', 'архивная API-документация и код компонента', 'в этом запросе был cache hit', 'сверить внешние условия'],
|
||||
['Что сохраняется из $arResult?', 'SetResultCacheKeys и конкретный компонент', 'шаблон не влияет на страницу', 'проверить template contract'],
|
||||
['Почему страница медленная?', 'реальный разрешённый артефакт конкретного маршрута', 'достаточно прочитать API', 'собрать источник наблюдения отдельно'],
|
||||
['Можно ли менять кеш?', 'полный перечень входов и rollback', 'очистка всего кеша безопасна', 'сделать один обратимый diff'],
|
||||
]),
|
||||
h2('Ключ — это часть публичного поведения'),
|
||||
p('Когда результат зависит от группы посетителя, языка, витрины, прав, фильтра или другого внешнего условия, это не «мелкая деталь кеша». Это часть условия, по которому один HTML допустим, а другой нет. Нельзя угадать полный список по названию компонента. Его получают из конкретного output contract: что меняет шаблон и откуда это значение приходит. Иногда вход уже в `$arParams`, иногда его нужно передать дополнительно, иногда компонент вообще нельзя рассматривать изолированно.'),
|
||||
p('Полезное правило ревью: каждый фрагмент HTML либо одинаков для всех состояний ключа, либо имеет названную зависимость. Если оба ответа не доказаны, ключ считаем неполным. Это строгий, но недорогой stop condition. Он останавливает попытку «ускорить» страницу до того, как та станет показывать один вариант другим посетителям. Сначала корректность выдачи, потом экономия работы.'),
|
||||
figure('/assets/editorial/2022/bitrix-performance-2022-cache-contract.svg', 'Схема контракта ключа встроенного кеша Bitrix: SITE_ID, имя компонента, шаблон и arParams — базовые части; внешнее условие, влияющее на HTML, нужно проверить отдельно. При неполном входе изменение останавливается.', 'Схема пересказывает границы API и добавляет редакторское правило stop condition. Она не показывает настоящий cache hit, файловое хранилище или статистику сервера.'),
|
||||
h2('Проверяемый пример: один пропущенный вход меняет решение'),
|
||||
p('Fixture специально работает не с Bitrix, а с объектом `createTeachingRequest`. В полном варианте есть `visitor-segment`; в неполном он отсутствует среди declared parts, хотя требуется output contract. Модель возвращает `stop-incomplete-cache-contract` и блокирует планируемую замену шаблона. Это не эмуляция `StartResultCache`, не генерация HTML и не тест платформы. Это проверка нашего правила: неизвестную зависимость нельзя замаскировать оптимизацией.'),
|
||||
code(fixtureExample),
|
||||
p('Обратите внимание на язык результата. `declared-cache-reuse` не называется hit. `declared-cache-build` не называется miss. Эти слова часто кажутся безобидными, но они уже сообщают факт о хранении и реальном вызове. Fixture хранит только то, что было задано как учебный вход. Такой запрет делает пример пригодным для ревью: никто не сможет перенести его output в отчёт о production без нового источника.'),
|
||||
h2('Маршрут: симптом → причина → проверка → действие'),
|
||||
ol([
|
||||
'<strong>Симптом.</strong> Команда видит непредсказуемую страницу или хочет менять кеш, но не может перечислить, от чего зависит HTML.',
|
||||
'<strong>Причина.</strong> Контракт ключа заменён предположением: «в параметрах наверняка всё есть» или «кеш общий для всех».',
|
||||
'<strong>Проверка базы.</strong> Сверьте документированный набор `SITE_ID`, component, template и `$arParams` с конкретным местом вызова.',
|
||||
'<strong>Проверка внешних условий.</strong> Для каждого отличающегося output назовите источник и способ включить его в договорённость компонента.',
|
||||
'<strong>Действие.</strong> При полном перечне планируйте один diff и rollback; при пропуске остановите изменение и уточните owner/contract.',
|
||||
'<strong>Повтор.</strong> После diff повторите тот же список входов. Новый вариант страницы или новая зависимость требуют отдельного решения.',
|
||||
]),
|
||||
h2('Почему шаблон нельзя вычеркнуть из разговора'),
|
||||
p('Шаблон — не декоративный хвост компонента. Он получает результат и формирует HTML, поэтому именно здесь часто видно, какие данные реально влияют на выход. Но это не означает, что шаблон надо объявить причиной медленной страницы. Без профиля нельзя назвать его длительность, а без анализа входов нельзя понять, что именно потребляет результат. Правильный промежуточный вывод уже полезен: «у шаблона есть владелец и output contract; его нужно рассматривать отдельно от ключа». '),
|
||||
p('Статья не предлагает добавлять логи в каждый шаблон и не выдумывает инструмент Bitrix. В конкретном проекте способ наблюдения выбирается по доступной среде и правилам доступа: исходный код, локальный debug-инструмент, разрешённый профиль или воспроизводимый стенд. Важно сохранить границу: evidence надо привязать к одному владельцу и одному маршруту, а не к слову «страница». '),
|
||||
h2('Управляемый кеш не отменяет договор'),
|
||||
p('Курс Bitrix различает компонентное, неуправляемое и управляемое кеширование. Из этого не следует, что управляемый режим сам определит все смысловые зависимости HTML. Он относится к обновлению данных и механизму cache dependencies, а вопрос о том, что участвует в выходе компонента, остаётся проектным. Если HTML зависит от условия, которое не вошло в key contract, автоматическое обновление не превращает это условие в известное.'),
|
||||
p('Поэтому на ревью полезно держать два списка: «что делает результат другим» и «когда результат должен обновиться». Первый описывает ключ, второй — invalidation. Смешивание списков порождает ложные решения: добавляют срок жизни вместо зависимости или очищают кеш вместо исправления output. У каждого списка должен быть владелец, иначе следующий компонент будет копировать случайный набор параметров.'),
|
||||
h2('Rollback и границы решения'),
|
||||
p('Хороший rollback не обещает откатить платформу. Он возвращает конкретное изменение: прежний параметр компонента, прежний template owner или ранее названный key part. В модели rollback возвращает `catalog-grid` и снова выдаёт evidence без длительности и SQL. Это напоминает важное свойство: откат проверяет форму нашего решения, но не измеряет продукт. После реального rollback всё равно нужен отдельный повторный сбор артефакта на согласованной границе.'),
|
||||
p('Если после изменения появился другой output или changed condition, не используйте старое сравнение. Новый контракт нужно оформить явно. Иначе можно получить красивую историю о производительности, где сравниваются разные посетители, разные параметры или разные шаблоны. Наличие новых чисел не делает такую историю надёжнее.'),
|
||||
h2('Ограничение и следующий шаг'),
|
||||
p('Эта статья не содержит production-кейса, SQL, пользовательского сегмента, срока кеша, hit-rate или метрики. `visitor-segment` — названное учебное условие, а не свойство реального сайта. Источники не доказывают работу произвольного самописного компонента и не заменяют документацию его версии. Их роль ограничена историческим API Bitrix, существовавшим до ноября 2022 года.'),
|
||||
p('Следующий шаг — выбрать ровно один компонент и написать его output contract на одну страницу: какие данные меняют HTML, что формирует шаблон, что живёт в `$arParams`, что приходит извне и как выглядит rollback. Только после этого имеет смысл выбирать реальный инструмент наблюдения. Такая подготовка короче большой оптимизации, но оставляет команде проверяемую основу.'),
|
||||
h2('Историческая граница ноября 2022'),
|
||||
p('Ссылки ниже ведут на официальные страницы Bitrix в архивных снимках июня, августа и сентября 2022 года. Это важно: текущая документация могла поменять формулировки или примеры. В тексте не используются поздние советы платформы и не приписывается архивному API поведение, которого он не описывает.'),
|
||||
], commonSources);
|
||||
|
||||
const field = revision({ slug: 'editorial-2022-11-field-bitrix-performance', title: 'Производительность Bitrix-страницы: диагностика, решение и проверка', categories: ['Bitrix', 'Производительность'], cover: '/assets/editorial/2022/bitrix-performance-2022-diagnosis-rollback.svg', excerpt: 'Полевой маршрут для случая, когда «тормозит Bitrix»: собрать один честный симптом, сузить границу до запроса, компонента, кеша или шаблона и не потерять rollback.', readingMinutes: 12 }, [
|
||||
p('Когда на вопрос «что тормозит?» приходит ответ «Bitrix», расследование уже потеряло форму. Платформа становится удобным виновником, а конкретный URL, компонент и шаблон остаются без владельца. Цена видна быстро: один человек очищает кеш, другой меняет вёрстку, третий спорит о сервере. После нескольких правок нельзя сказать, что проверяли и какое изменение можно безопасно вернуть.'),
|
||||
p('Полевой маршрут начинается с дисциплины: не искать эффект, а ограничить вопрос. Мы не знаем реальную причину, пока не собрали артефакт. Зато можем записать симптом, выбрать одну границу и исключить неверные выводы. Учебная модель этого пакета помогает проверить последовательность решения: неполный cache contract останавливает diff; полный допускает планирование одного изменения с rollback. Она не создаёт профилировщик и не сообщает время страницы.'),
|
||||
h2('Сделайте симптом пригодным для передачи'),
|
||||
p('Плохой симптом: «каталог медленный». Хорошая рабочая заготовка: «для согласованного маршрута и варианта страницы требуется отделить component/cache/template до изменения». В ней нет придуманных миллисекунд, пользователей или результата. Но есть граница, на которую можно назначить владельца. Если реальный пользовательский сигнал известен, запишите его дословно в рабочем тикете рядом с условиями доступа; статья не подменяет такой сигнал своим примером.'),
|
||||
p('Дальше фиксируется исходный контракт: route, вариант, компонент, template owner, параметры и условия, влияющие на HTML. Не нужно сразу знать ответ. Важно, чтобы неизвестное поле не исчезло из записи. Например, неизвестный `visitor-segment` — причина остановиться и выяснить, меняет ли он output. Это дешевле, чем отправить на production правку ключа и только потом обнаружить, что разные посетители получили общий результат.'),
|
||||
table('Матрица диагностики без выдуманного trace', ['Наблюдение', 'Разрешённая гипотеза', 'Чего нельзя утверждать', 'Следующая проверка'], [
|
||||
['Есть один маршрут и вариант', 'можно выбрать owner boundary', 'исполнялся конкретный SQL', 'найти подключение компонента'],
|
||||
['Назван component и template', 'можно читать их contract отдельно', 'один из них уже причина', 'сверить параметры и output'],
|
||||
['Ключ полный по объявлению', 'можно планировать узкий diff', 'на сервере был cache hit', 'собрать реальный артефакт при необходимости'],
|
||||
['Ключ неполный', 'изменение кеша опасно', 'кеш не участвует совсем', 'назвать недостающий input'],
|
||||
['Diff обратим', 'можно повторить прежний вход', 'регрессия устранена', 'сравнить тот же evidence contract'],
|
||||
]),
|
||||
h2('Разделите четыре ветки до первого решения'),
|
||||
p('Ветка запроса отвечает за вход: какой маршрут и вариант вообще рассматриваются. Ветка компонента — за имя, параметры и владеющий код. Ветка кеша — за зависимость результата и правила обновления. Ветка шаблона — за output, который формируется из результата. Каждая ветка может дать свой артефакт, но один артефакт нельзя механически переносить на остальные. Список шаблона не является списком SQL, а документированный key contract не является результатом network замера.'),
|
||||
p('Если команда использует настоящий профилировщик или лог, это отдельная работа: фиксируются версия, среда, вход, способ запуска и владельцы данных. Здесь таких артефактов нет, поэтому мы не моделируем их поля. Даже «скорее всего запрос в базе» — лишняя фраза, если она не подтверждена. Честное «пока неизвестно» не замедляет расследование; оно защищает от необратимой правки не той ветки.'),
|
||||
figure('/assets/editorial/2022/bitrix-performance-2022-diagnosis-rollback.svg', 'Маршрут диагностики: сохранить симптом, разделить request, component, cache и template, проверить один объявленный факт, сделать малый diff или остановиться при неполном evidence, затем выполнить rollback и повторить контракт.', 'Это схема принятия решения. В ней нет реального Bitrix-запроса, SQL, браузерного профиля, тайминга, cache hit-rate или результата пользователя.'),
|
||||
h2('Проверяемый пример: stop condition важнее красивого diff'),
|
||||
p('Fixture показывает две развилки. В complete contract перечислены базовые части и `visitor-segment`, поэтому учебная модель разрешает запланировать замену template owner и помнит, что вернуть. В incomplete contract этого входа нет. Модель не пытается подобрать TTL или «перестроить кеш»: она возвращает `change-blocked`. Такой отрицательный пример важнее happy path, потому что он проверяет границу решения.'),
|
||||
code(fixtureExample),
|
||||
p('Запуск проверяет тринадцать assertions о локальных объектах: runtime не стартовал, SQL и duration не собирались, неполный вход назван, change blocked не меняет модель, planned change создаёт отдельную версию и содержит rollback, а rollback возвращает прежний template. В этих assertions нет «страница стала быстрее», «кеш работает» или «профиль чистый». Если нужно проверить такие утверждения, понадобятся реальные, отдельно сохранённые evidence конкретного проекта.'),
|
||||
h2('Маршрут: симптом → причина → проверка → действие'),
|
||||
ol([
|
||||
'<strong>Симптом.</strong> Сохраните один согласованный маршрут и вариант. Не добавляйте предполагаемые миллисекунды или SQL, если их никто не измерял.',
|
||||
'<strong>Причина как гипотеза.</strong> Выпишите четыре возможные границы: request, component, cache, template. Назначьте владельца каждой доступной.',
|
||||
'<strong>Проверка контракта.</strong> Прочитайте точку подключения компонента, его параметры и шаблон; отдельно назовите внешние условия output.',
|
||||
'<strong>Проверка кеша.</strong> Сопоставьте список с историческим API `StartResultCache`. Пропущенный input — stop condition, не повод выключить кеш.',
|
||||
'<strong>Действие.</strong> Выберите одно обратимое изменение на названной границе либо соберите недостающий артефакт без изменения кода.',
|
||||
'<strong>Rollback и повтор.</strong> Верните один diff при отрицательном результате и повторите исходный contract. Не сравнивайте другой маршрут под тем же названием.',
|
||||
]),
|
||||
h2('Какие «быстрые» исправления обычно делают хуже'),
|
||||
p('Первое — очистить весь кеш и считать исчезновение симптома объяснением. Это меняет состояние, но не показывает, какая зависимость была важна. Второе — выключить компонентный кеш без оценки output contract. Так можно убрать одну неизвестность и добавить другую нагрузку, а связь с исходной страницей всё равно не доказать. Третье — переписать шаблон только потому, что он виден в коде. Видимость не равна вине.'),
|
||||
p('Четвёртое — собрать все возможные инструменты одновременно. Логи, профили, network, мониторинг и SQL могут быть нужны, но их массовый запуск без вопроса создаёт шум и риск для данных. Начните с одного артефакта, который различит две гипотезы. Если он не различил, не притворяйтесь, что данных достаточно: вернитесь к контракту и выберите следующую границу. Это медленнее в первом абзаце и быстрее через неделю.'),
|
||||
h2('Решение должно быть уже диагноза'),
|
||||
p('После evidence легко захотеть исправить всё: ключ, TTL, шаблон, параметры и сервер. Но диагностика ценна только пока сохраняет различение. Если факт указывает на один отсутствующий input, действие — назвать его и проверить contract, а не менять пять компонентов. Если факт относится к template owner, diff должен оставаться в его зоне ответственности. Узкое решение проще проверить, проще отменить и проще объяснить в следующем PR.'),
|
||||
p('В описании изменения оставьте четыре строки: исходный symptom, использованный evidence, конкретный diff, rollback target. Этого достаточно, чтобы reviewer мог спросить о пробеле. Не пишите «ускорили Bitrix» — фраза шире материала. Корректнее: «уточнили контракт кеша компонента» или «заменили один template owner и повторили согласованный вход». Такой результат может быть скромнее, но он не обещает то, чего никто не измерял.'),
|
||||
h2('Когда остановка — правильный результат'),
|
||||
p('Остановитесь, если неизвестен маршрут, output меняется по неописанному условию, нет владельца компонента или предложенный инструмент не разрешён в среде. Не компенсируйте пробелы фразой «обычно Bitrix делает так». Историческая документация описывает APIs платформы, но не обязана знать конкретный самописный шаблон, интеграцию или версии проекта. Перенос общего правила на частный случай требует локального доказательства.'),
|
||||
p('Остановка превращается в полезную задачу, если назвать недостающий факт: «нужно выяснить, влияет ли X на HTML и где он должен жить в контракте»; «нужно подтвердить владельца шаблона»; «нужен разрешённый артефакт на одном URL». Такой вопрос можно отдать следующему человеку без легенды о производительности. И он не портит систему необратимой оптимизацией.'),
|
||||
h2('Источники и границы ноября 2022'),
|
||||
p('Архивные документы Bitrix ниже проверены как доступные до ноября 2022: API `StartResultCache`, `SetResultCacheKeys` и курс о видах кеширования. Они служат источником только для их собственных контрактов. В статье нет ссылок на нынешние страницы как на исторический факт, нет выдуманного локального кейса и нет технических утверждений о настоящей странице.'),
|
||||
p('Следующий шаг после этой статьи — заполнить матрицу для одного реального маршрута и согласовать, какой артефакт можно собирать безопасно. Лишь затем выбирайте инструмент и изменения. Так «производительность Bitrix-страницы» перестаёт быть названием для тревоги и становится последовательностью проверяемых шагов.'),
|
||||
], commonSources);
|
||||
|
||||
export const revisions = [practice, mechanism, field].map(({ proseLength, ...item }) => item);
|
||||
|
||||
function verifyFixture() {
|
||||
const report = runBitrixPerformanceFixture();
|
||||
const failed = Object.entries(report.assertions).filter(([, value]) => value !== true).map(([key]) => key);
|
||||
if (failed.length) { process.stderr.write('FAIL fixture: ' + failed.join(', ') + '\n'); process.exitCode = 1; return; }
|
||||
process.stdout.write('PASS fixture: ' + Object.keys(report.assertions).length + '/' + Object.keys(report.assertions).length + ' assertions\n');
|
||||
}
|
||||
if (process.argv.includes('--verify-fixture')) verifyFixture();
|
||||
if (process.argv.includes('--print-revisions')) process.stdout.write(JSON.stringify(revisions) + '\n');
|
||||
Reference in New Issue
Block a user