Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@
## 2026-07-10 - Remove unnecessary DOMPurify for performance
**Learning:** 애플리케이션이 `textContent`와 같은 안전한 DOM API만 사용하고 `innerHTML` 등의 위험한 싱크를 사용하지 않는다면 DOMPurify와 같은 라이브러리를 통해 Trusted Types 정책을 생성할 필요가 없음.
**Action:** 불필요한 번들 다운로드 및 스크립트 실행을 방지하기 위해 사용하지 않는 라이브러리를 식별하고 제거할 것.

## 2024-07-14 - SVG 및 LCP 이미지의 잘못된 decoding="async" 적용 방지
**Learning:** LCP(Largest Contentful Paint) 대상 이미지나 SVG 파일에 `decoding="async"`를 적용하면 렌더링이 지연되어 오히려 성능 저하를 일으키는 안티패턴이 됩니다. SVG는 디코딩이 아닌 파싱 과정을 거치며, LCP 이미지는 렌더링을 지연시키지 않아야 합니다.
**Action:** 오프스크린이거나 지연 로드되는 래스터 이미지에만 `decoding="async"`를 적용하고, SVG 및 우선순위가 높은 LCP 이미지에는 적용하지 않도록 합니다.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# CHANGELOG

## [Unreleased]
- **성능 최적화**: SVG 파일 및 최상단 LCP(Largest Contentful Paint) 대상 이미지에서 불필요한 `decoding="async"` 속성을 제거하여 브라우저의 파싱 및 초기 렌더링 지연을 방지했습니다.
- **보안 개선**: 컴포넌트 갤러리의 인라인 스크립트와 스타일을 외부 파일로 분리하고, 엄격한 Content-Security-Policy를 적용해 XSS 방어를 강화했습니다.
- **성능 회귀 복원**: 오프스크린 `.section` 렌더링을 `content-visibility: auto`로 지연하고, 일반 섹션은 600px·콘텐츠가 큰 DIKW/projects 섹션은 1000px의 `contain-intrinsic-size` placeholder를 유지해 초기 렌더링 비용과 스크롤바 이동을 함께 줄였습니다.
- **보안 개선**: Trusted Types 기반 CSP 강화: 잠재적인 DOM 기반 XSS 공격을 방지하기 위해 `require-trusted-types-for 'script'` 지시어 추가
Expand Down
9 changes: 4 additions & 5 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<a href="#top" class="skip-link" data-i18n="nav.skipToContent">본문으로 건너뛰기</a>
<header class="site-header">
<a class="brand" href="#top" aria-label="Contextual Wisdom Lab home">
<img src="assets/context-wisdom-lab-avatar.svg" alt="" width="44" height="44" decoding="async">
<img src="assets/context-wisdom-lab-avatar.svg" alt="" width="44" height="44">
<span>Contextual Wisdom Lab</span>
</a>
<nav class="site-nav" aria-label="Primary navigation">
Expand Down Expand Up @@ -64,7 +64,7 @@ <h1 data-i18n="hero.title">맥락지혜 연구실</h1>
</div>

<div class="hero-visual">
<img class="context-art" src="assets/context-thread-map.svg" alt="" aria-hidden="true" width="760" height="560" fetchpriority="high" decoding="async">
<img class="context-art" src="assets/context-thread-map.svg" alt="" aria-hidden="true" width="760" height="560" fetchpriority="high">
<div class="ladder" role="list" aria-label="Data to wisdom ladder">
<div class="ladder-row" role="listitem">
<span>Data</span>
Expand Down Expand Up @@ -194,7 +194,7 @@ <h2 data-i18n="dikw.title">DIKW는 판단 흐름을 점검하는 질문입니다
</p>
</div>
<!-- ⚡ Bolt: Lazy load off-screen image -->
<img class="section-diagram" src="assets/dikw-checkpoints.svg" alt="" aria-hidden="true" loading="lazy" width="1120" height="330" decoding="async">
<img class="section-diagram" src="assets/dikw-checkpoints.svg" alt="" aria-hidden="true" loading="lazy" width="1120" height="330">
<div class="dikw-grid">
<article>
<span>Data</span>
Expand Down Expand Up @@ -272,7 +272,7 @@ <h2 data-i18n="references.title">참고문헌</h2>
<section id="logo" class="section logo-story">
<div class="logo-mark">
<!-- ⚡ Bolt: Lazy load off-screen image -->
<img src="assets/context-wisdom-lab-avatar.svg" alt="Contextual Wisdom Lab square mark" loading="lazy" width="500" height="500" decoding="async">
<img src="assets/context-wisdom-lab-avatar.svg" alt="Contextual Wisdom Lab square mark" loading="lazy" width="500" height="500">
</div>
<div>
<h2 data-i18n="logo.title">로고는 흩어진 기록을 하나의 흐름으로 묶습니다</h2>
Expand Down Expand Up @@ -425,7 +425,6 @@ <h2 data-i18n="work.title">연구에서 제품으로</h2>
loading="lazy"
width="920"
height="260"
decoding="async"
>
<p>
<span data-i18n="footer.founded">Founded by</span>
Expand Down
13 changes: 11 additions & 2 deletions tests/test_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,18 @@ def test_tall_sections_reserve_larger_intrinsic_block_size():


def test_images_decode_without_blocking_rendering():
"""All site images opt into asynchronous decoding."""
"""All raster site images opt into asynchronous decoding, but SVGs and high priority images do not."""
parser = _ImageParser()
parser.feed(INDEX.read_text(encoding="utf-8"))

assert parser.images
assert all(image.get("decoding") == "async" for image in parser.images)
for image in parser.images:
src = image.get("src", "")
fetchpriority = image.get("fetchpriority")

# SVGs and high priority images should NOT have decoding="async"
if src.endswith(".svg") or fetchpriority == "high":
assert image.get("decoding") is None, f"Image {src} should not have decoding='async'"
else:
# Raster images should have decoding="async"
assert image.get("decoding") == "async", f"Image {src} missing decoding='async'"
Loading