#!/usr/bin/env bash # # Lighthouse run against the production image, reproducing the measurement # behind MAN-109. Not a CI gate — SPEC.md §14.1 pins the five blocking jobs and # §22 governs when new capability is added. This is the recipe so the numbers # can be reproduced instead of remembered. # # Usage: # scripts/perf/lighthouse.sh [output-dir] # # Environment: # BASE_URL origin to audit (default http://127.0.0.1:8000) # TARGET label for the output subtree (default local) # RUNS runs per page and preset (default 3) # PRESETS space-separated preset list (default "mobile desktop") # # Lighthouse LCP moves by a few tenths of a second between runs on the same # build, so a single run cannot support a before/after comparison. Every page # is audited RUNS times per preset and the run holding the median LCP is the # one reported — averaging across runs would describe a page that never # existed. # # Expects a site already answering on $BASE_URL. To raise one from scratch: # # docker build -t amare-app:ci . # docker run -d --name amare-web -p 8000:8000 \ # -e APP_ENV=production -e APP_DEBUG=false -e APP_KEY="$APP_KEY" \ # -e DB_CONNECTION=pgsql -e DB_HOST=host.docker.internal -e DB_PORT=5432 \ # -e DB_DATABASE=amare -e DB_USERNAME=amare -e DB_PASSWORD=secret \ # -e SESSION_DRIVER=database -e CACHE_STORE=database -e QUEUE_CONNECTION=database \ # --add-host=host.docker.internal:host-gateway \ # -v "$(pwd)/storage/app/public:/app/storage/app/public" \ # amare-app:ci # # Seed content and generate the responsive variants first, and run both from # the host rather than inside the container. ContentSeeder guards itself with # an allow-list of local/staging/testing (database/seeders/ContentSeeder.php), # so under the container's APP_ENV=production it is a deliberate no-op and # Lighthouse would end up measuring empty pages. Skipping # `media:generate-variants` inflates LCP by roughly 2.5 s on the home page, # because the originals are served at full size: # # php artisan db:seed --class=ContentSeeder --force # php artisan media:generate-variants # set -euo pipefail BASE_URL="${BASE_URL:-http://127.0.0.1:8000}" BASE_URL="${BASE_URL%/}" TARGET="${TARGET:-local}" RUNS="${RUNS:-3}" PRESETS="${PRESETS:-mobile desktop}" OUT_DIR="${1:-storage/app/lighthouse}/${TARGET}" # Lighthouse needs a Chrome binary. Playwright's is already on disk after # `npx playwright install chromium`; fall back to a system Chrome. if [[ -z "${CHROME_PATH:-}" ]]; then PLAYWRIGHT_CHROME=$(find "${HOME}/Library/Caches/ms-playwright" "${HOME}/.cache/ms-playwright" \ -maxdepth 3 -name 'Google Chrome for Testing' -type f 2>/dev/null | head -1 || true) if [[ -n "${PLAYWRIGHT_CHROME}" ]]; then export CHROME_PATH="${PLAYWRIGHT_CHROME}" fi fi mkdir -p "${OUT_DIR}" PAGES=( "/:home" "/servicos:servicos" "/portfolio:portfolio" "/portfolio/casamento-ana-lucas:portfolio-detalhe" "/sobre:sobre" "/contato:contato" ) # A route that answers 404 still produces a Lighthouse report, and the error # page is light enough to score well — the heaviest route on the site would be # reported as excellent and nobody would notice. Refuse to measure anything # that is not a 200 before spending minutes on the audit. echo "preflight against ${BASE_URL}" for entry in "${PAGES[@]}"; do path="${entry%%:*}" code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 20 "${BASE_URL}${path}" 2>/dev/null || true)" if [[ "${code}" != "200" ]]; then echo "preflight failed: ${BASE_URL}${path} answered HTTP ${code:-}, expected 200" >&2 exit 1 fi echo " ok ${path}" done for preset in ${PRESETS}; do # The mobile preset is Lighthouse's default and rejects an explicit # --preset flag, so only desktop is passed through. preset_flags=() if [[ "${preset}" != "mobile" ]]; then preset_flags+=("--preset=${preset}") fi for entry in "${PAGES[@]}"; do path="${entry%%:*}" name="${entry##*:}" for run in $(seq 1 "${RUNS}"); do echo "auditing ${name} ${preset} run ${run}/${RUNS} (${BASE_URL}${path})" # Default preset: simulated mobile throttling, 150 ms RTT, # ~1.6 Mbps, 4x CPU slowdown. # ${array[@]+...} keeps `set -u` from treating an empty array as # unbound, which bash 3.2 (the macOS system bash) still does. npx --yes lighthouse@12 "${BASE_URL}${path}" \ --quiet \ ${preset_flags[@]+"${preset_flags[@]}"} \ --output=json --output=html \ --output-path="${OUT_DIR}/${name}-${preset}-run${run}" \ --chrome-flags="--headless=new --no-sandbox" done done done echo echo "reports written to ${OUT_DIR}" # The seeder is recorded because it decides the byte weight of every hero # image: a before/after comparison across different fixtures measures nothing. node scripts/perf/summarize-lighthouse.mjs "${OUT_DIR}" \ --target="${TARGET}" \ --base-url="${BASE_URL}" \ --commit="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)$(git diff --quiet HEAD 2>/dev/null || echo '+alterações não commitadas')" \ --seeder="${SEEDER:-ContentSeeder}" \ > "${OUT_DIR}/summary.md" echo "summary written to ${OUT_DIR}/summary.md" echo "SPEC.md §6.6 targets: LCP <= 2.5s, CLS <= 0.1, INP <= 200ms, 0 console errors"