.htaccess: мифы о производительности

Расхожее мнение: «.htaccess тормозит сайт — лучше его не использовать». Правда сложнее: для нормального небольшого файла оверхед микроскопический и в реальных условиях незаметен. Но несколько конкретных паттернов действительно замедляют — разберём каждый.

1. Правда ли .htaccess тормозит?

Короткий ответ: для нормального небольшого .htaccess — нет, оверхед микроскопический и на практике незаметен. Для гигантского файла, или если .htaccess лежит по копии в каждой подпапке, или если в начале правил стоит широкий catch-all .* — да, реальное замедление.

Миф живёт потому, что Apache читает .htaccess на каждый запрос — это правда. Но само по себе это ещё не значит «медленно»: ОС кэширует файл в памяти, и при маленьком .htaccess стоимость парсинга ничтожна. Проблема начинается при больших файлах или неудачной структуре правил.

2. Как Apache читает .htaccess

При каждом запросе Apache проходит путь от DocumentRoot вниз до каталога, где лежит запрошенный файл. В каждом каталоге на этом пути он проверяет, есть ли .htaccess, и если есть — читает и парсит его заново. Это означает:

  • Запрос к /blog/2024/post.html → Apache читает .htaccess из корня, из /blog/ и из /blog/2024/, если они существуют.
  • Статика (CSS / JS / картинки) тоже проходит этот путь — .htaccess читается и на каждый хит по статике.
  • Правила в конфиге сервера (<Directory> в vhost) парсируются один раз при старте Apache и живут в памяти.

Именно поэтому перенос правил в конфиг с AllowOverride None — самая радикальная оптимизация. Подробно о механизме — статья «AllowOverride и .htaccess vs httpd.conf».

3. Что реально медленно

  • Гигантский .htaccess — файл на сотни строк с десятками правил. Парсинг при каждом запросе начинает давать заметный оверхед.
  • .htaccess в каждой подпапке — Apache суммирует работу по всем файлам на пути к запрошенному ресурсу. Три файла на пути — тройная стоимость парсинга.
  • Широкий catch-all .* или (.+) в начале правил — такое правило проверяется на каждый запрос, включая статику. Если оно никогда не срабатывает — это лишняя работа впустую. Проверить тяжёлые правила — симулятор /perf-sim/.
  • Десятки RewriteRule без раннего выхода — каждый запрос к статике прогоняется через все правила до конца.
  • Отсутствие раннего выхода для существующих файлов — без проверки RewriteCond %{REQUEST_FILENAME} !-f / !-d запрос к реально существующему файлу всё равно проходит все rewrite-правила.

4. Грубые ориентиры по замерам

  • Пустой .htaccess vs файл с 1000 строк правил — разница заметна: несколько миллисекунд на запрос при нагрузке. На небольшом трафике этого не увидите; на высоконагруженном — накапливается.
  • 10–20 правил — оверхед в пределах статистической погрешности на большинстве серверов.
  • Статика (CSS / JS / картинки) — тоже читает .htaccess при каждом хите. Если статики много и она часто запрашивается — ранний выход для существующих файлов даёт реальный прирост.
  • ОС кэширует файл в памяти — дисковое чтение дешёвое; дорог именно парсинг при большом файле.

5. Как ускорить

① Перенести правила в конфиг сервера / vhost с AllowOverride None — самый эффективный шаг, если есть доступ к конфигу (VPS / выделенный сервер). Правила обрабатываются один раз при старте, а не при каждом запросе. Подробно: «AllowOverride и .htaccess vs httpd.conf».

httpd.confкопировать
<Directory /var/www/example.com/public_html>
    AllowOverride None
    Require all granted

    RewriteEngine On
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . index.php [L]
</Directory>

② Не плодить .htaccess в подкаталогах — держите один файл в корне. Каждый лишний .htaccess на пути к файлу — дополнительный парсинг при каждом запросе.

③ Ранний выход для существующих файлов — добавьте в начало rewrite-блока условия, чтобы реальные файлы и каталоги не прогонялись через все правила:

.htaccessкопировать
RewriteEngine On

# если файл или каталог реально существуют — не трогать (статика, etc.)
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]

# остальные запросы — во фронт-контроллер
RewriteRule . index.php [L]

<FilesMatch> вместо широких RewriteRule — для добавления заголовков к статике используйте <FilesMatch "\.(css|js|webp)$">, а не RewriteRule. <FilesMatch> эффективнее: Apache матчит имя файла один раз, без прохода по всем rewrite-правилам.

⑤ Убрать неиспользуемые и устаревшие директивы — «мёртвые» строки всё равно парсируются. Проверить файл: линтер /check/.

6. Мелкие мифы

  • Options -Indexes не замедляет — это просто флаг, устанавливаемый при парсинге; влияния на производительность ответа практически нет.
  • Много Header set не тормозит — заголовки формируются Apache и так при каждом ответе; добавление нескольких строк через mod_headers ничтожно дёшево.
  • mod_rewrite сам по себе быстрый — медленными делают его плохие правила: catch-all в начале, который проверяется на каждый запрос, но никогда не срабатывает.
  • <IfModule>-обёртки почти бесплатны — проверка, загружен ли модуль, происходит один раз при парсинге и кэшируется.
  • .htaccess НЕ кэшируется Apache между запросами — именно поэтому он читается каждый раз. Но ОС кэширует содержимое файла в памяти, так что дисковое чтение обычно дёшево. Главная стоимость — парсинг текста при большом файле.

7. Чеклист

  • .htaccess не гигантский (до ~50 строк реальных директив)?
  • ② Нет копий .htaccess в каждой подпапке — один в корне?
  • ③ Нет catch-all .* или (.+) в самом начале правил? — проверьте симулятором /perf-sim/
  • ④ Есть ранний выход для существующих файлов (RewriteCond %{REQUEST_FILENAME} !-f / !-d)?
  • ⑤ Если есть доступ к конфигу сервера — правила перенесены туда с AllowOverride None? — статья AllowOverride
  • ⑥ Нет мёртвых директив, которые Apache игнорирует или не поддерживает? — /check/