diff --git a/.jules/bolt.md b/.jules/bolt.md index 65f2457..11eed41 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -16,3 +16,7 @@ ## 2026-08-05 - Separate image fetch and decode hints **Learning:** The HTML Standard defines `decoding` as a preference hint whose missing-value default is `auto`; it does not guarantee a particular main-thread or background-thread execution path. `fetchpriority` independently influences fetch priority. Removing `decoding="async"` must therefore not be described as a guaranteed LCP improvement. **Action:** Let eager first-viewport images use the user agent's `auto` decode strategy, retain `decoding="async"` for explicitly lazy images, and require non-vacuous tests for eager, lazy, and single high-priority LCP-candidate sets. Record the evidence and measurement limits in `docs/doctoring/image-rendering-hints.md`. + +## 2026-08-08 - 애니메이션 성능을 위해 top/left 대신 transform 사용 +**Learning:** 이 skip-link 전환을 `top`에서 `transform`으로 바꾸면 애니메이션 중 레이아웃 재계산을 피하는 데 유리합니다. 개발자 도구에서 이 전환의 Layout 이벤트가 관찰되지 않았지만, 브라우저·장치별 GPU 가속이나 메인 스레드 비용 0ms를 보장하지는 않습니다. +**Action:** 레이아웃 속성 대신 `transform` 전환을 우선 검토하고, 성능 효과는 브라우저별 측정으로 확인하며 절대적인 GPU·비용 보장으로 기록하지 않습니다. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bb1089..2144e85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # CHANGELOG ## [Unreleased] +- **성능 개선**: `.skip-link` 애니메이션을 `top`에서 `transform: translateY()`로 변경하여 전환 중 레이아웃 재계산을 줄일 수 있도록 했습니다. 실제 효과는 브라우저별 측정 대상입니다. - **렌더링 힌트 정합성**: 첫 화면의 eager 이미지와 단일 LCP 후보에서 강제 `decoding="async"`를 제거해 HTML 표준의 기본 `auto` 판단에 맡기고, 지연 로드 이미지에는 비동기 디코딩 힌트를 유지했습니다. 정적 테스트가 eager, lazy, LCP 후보 집합의 존재와 조합을 검증하며, 실제 LCP 효과는 배포 후 실측 대상으로 유지합니다. - **UX/접근성 개선**: 프로젝트 카드의 클릭 영역을 카드 전체로 확장하여 사용자 편의성을 높였습니다. 태그를 확장하는 대신 가상 요소(pseudo-element) 겹침 방식을 사용하여 스크린 리더 접근성을 유지했습니다. - **보안 개선**: 컴포넌트 갤러리의 인라인 스크립트와 스타일을 외부 파일로 분리하고, 엄격한 Content-Security-Policy를 적용해 XSS 방어를 강화했습니다. diff --git a/styles.css b/styles.css index e3de699..f7697d8 100644 --- a/styles.css +++ b/styles.css @@ -818,19 +818,21 @@ main:focus-visible { .skip-link { position: absolute; - top: -40px; + top: 0; left: 0; + transform: translateY(-100%); background: var(--ink); color: var(--white); padding: 8px 16px; z-index: 100; font-weight: 800; text-decoration: none; - transition: top 0.2s ease-in-out; + /* ⚡ Bolt: Animate transform instead of top to prevent layout thrashing */ + transition: transform 0.2s ease-in-out; } .skip-link:focus-visible { - top: 0; + transform: translateY(0); } @media (prefers-reduced-motion: reduce) { diff --git a/tests/test_styles.py b/tests/test_styles.py index 916bc01..7a099da 100644 --- a/tests/test_styles.py +++ b/tests/test_styles.py @@ -102,3 +102,17 @@ def test_project_cards_are_fully_clickable_via_pseudo_element() -> None: after_rule = _rule(".project-grid h3 a::after") assert "position: absolute;" in after_rule assert "inset: 0;" in after_rule + + +def test_skip_link_animates_transform_not_top() -> None: + """Skip link must animate the compositor-only transform, never top.""" + rule = _rule(".skip-link") + + assert "transform: translateY(-100%);" in rule + transition = re.search(r"(?m)^\s*transition\s*:\s*([^;]+);", rule) + assert transition is not None + assert "transform" in transition.group(1) + assert "top" not in transition.group(1) + + focus_rule = _rule(".skip-link:focus-visible") + assert "transform: translateY(0);" in focus_rule