# 03. 성능 · 브라우저 함정 · 폴백

> "SVG 필터는 예쁘다"와 "SVG 필터를 프로덕션에 넣어도 된다"는 다른 문제다. 이 문서는 후자를 위한 것이다.

---

## 1. 비용 모델 — 무엇이 비싼가

### 1.1 기본 공식

```
프레임당 비용 ≈ (필터 영역 픽셀 수) × (프리미티브 수 × 프리미티브별 픽셀당 연산) × (재계산 빈도)
```

세 항 모두가 곱해진다. 하나만 줄여도 효과가 크고, 셋 다 방치하면 폭발한다.

### 1.2 프리미티브별 비용 등급

| 등급 | 프리미티브 | 픽셀당 연산 | 비고 |
|---|---|---|---|
| **매우 저렴** | `feOffset`, `feFlood`, `feMerge`, `feTile` | 복사/상수 | 사실상 공짜 |
| **저렴** | `feColorMatrix`, `feComponentTransfer`, `feBlend`, `feComposite` | 산술 수 회 | 마음껏 써도 됨 |
| **보통** | `feGaussianBlur`, `feDropShadow` | 분리형 컨볼루션 (반경 비례) | 반경이 커지면 급증 |
| **비쌈** | `feMorphology`, `feDisplacementMap` | 이웃 탐색 / 랜덤 액세스 | 캐시 미스 다발 |
| **매우 비쌈** | `feTurbulence`, `feConvolveMatrix`, `feDiffuseLighting`, `feSpecularLighting` | Perlin 반복 / N×N 커널 / 노멀 계산 | **애니메이션 금지 후보** |

`feTurbulence`는 별도로 언급할 가치가 있다. Firefox 버그 트래커에 "far too slow"로 15년 넘게 올라와 있고, 최신 노트북에서도 큰 면적에 걸면 CPU 코어를 즉시 포화시킨다는 보고가 있다. **`numOctaves`가 1 늘어날 때마다 노이즈 계산이 한 층씩 더 쌓인다.** 5 이상은 시각적 이득 대비 손해다.

### 1.3 필터 영역이 곱해지는 방식

`<filter>`의 `width`/`height`는 **처리할 픽셀 수를 직접 결정**한다.

| 필터 영역 | 원본 대비 픽셀 수 |
|---|---|
| `100% × 100%` | 1.00× |
| `120% × 120%` (기본값) | 1.44× |
| `160% × 160%` | 2.56× |
| `180% × 180%` | 3.24× |
| `200% × 200%` | 4.00× |

> 노이즈 오버레이처럼 영역 확장이 필요 없는 필터는 **반드시 `x="0%" y="0%" width="100%" height="100%"`** 로 줄여라. 그것만으로 30% 절약이다.

### 1.4 재계산이 일어나는 시점

필터 결과는 요소가 변하지 않는 한 캐시된다. 아래 상황에서 캐시가 무효화된다.

| 상황 | 재계산 |
|---|---|
| 필터 어트리뷰트 변경 (`baseFrequency`, `scale`, `seed`, `stdDeviation`) | ✅ 전체 |
| `flood-color` / `lighting-color` 변경 | ✅ 전체 (단, 형태 계산은 재사용 가능) |
| 필터 걸린 요소의 **자식이 변함** (텍스트, 애니메이션) | ✅ 전체 |
| 필터 걸린 요소의 **크기 변경** | ✅ 전체 |
| 필터 걸린 요소의 `transform` / `opacity` 변경 | ⚠️ 엔진·상황에 따라 다름. 합성 레이어로 승격되면 재사용되지만, 그렇지 않으면 매 프레임 재계산 |
| `backdrop-filter`에서 **배경이 스크롤/변화** | ✅ 매 프레임 |

> **가장 흔한 성능 사고**: 필터 걸린 요소를 `transform`으로 움직이는 것. 브라우저가 캐시된 레이어를 합성하는 대신 매 프레임 필터 전체를 다시 돌릴 수 있다. **필터와 모션은 서로 다른 요소에 분리하라.**

```html
<!-- ❌ 같은 요소에 필터 + 모션 -->
<div class="blob" style="filter:url(#goo); animation: move 3s infinite"></div>

<!-- ✅ 바깥이 움직이고, 안쪽이 필터를 가진다 (혹은 그 반대) -->
<div class="mover" style="animation: move 3s infinite">
  <div class="blob" style="filter:url(#goo)"></div>
</div>
```

### 1.5 `will-change: filter`를 쓰지 마라 (대부분의 경우)

- `will-change: filter`는 요소를 별도 합성 레이어로 승격시키고 **메모리를 상시 점유**한다.
- 필터 자체는 여전히 CPU/GPU에서 재계산된다 — 승격이 필터 연산을 없애주지 않는다.
- 다수 요소에 걸면 레이어 폭발로 오히려 느려진다.
- **예외**: 짧은 인터랙션 직전에 JS로 붙였다가 끝나면 제거하는 패턴은 유효하다.

```js
el.addEventListener('pointerenter', () => { el.style.willChange = 'filter'; });
el.addEventListener('transitionend', () => { el.style.willChange = 'auto'; });
```

### 1.6 하드웨어 가속의 현실

- 엔진별로 가속하는 필터가 다르다. Chrome이 가속하는 것, Firefox가 가속하는 것이 서로 다르고, 일부 안드로이드 GPU는 아무것도 가속하지 못한다.
- `feTurbulence`는 하드웨어 가속에 친화적이지 않은 것으로 알려져 있다.
- **Safari는 CSS 단축 필터(`blur()`, `drop-shadow()` 등)에서는 잘 동작하지만 SVG `<filter>`에서는 문제가 잦다.**
- Firefox에는 "하드웨어 가속을 켜면 대부분의 CSS/SVG 필터가 동작하지 않는다"는 장기 미해결 버그가 보고되어 있다.

**결론**: 가속을 전제로 설계하지 마라. **CPU에서 돌아도 부드러운 수준**으로 설계하고, 가속은 보너스로 취급하라.

---

## 2. CSS 필터 vs SVG 필터 — 선택 기준

| 판단 | 선택 |
|---|---|
| blur / brightness / contrast / saturate / hue-rotate / drop-shadow 만 필요 | **CSS 단축 필터** — 더 빠르고, 더 잘 가속되고, Safari에서 안전하며, 값 보간 애니메이션이 가능 |
| 노이즈, 변위, 채널 분리, 알파 임계, 조명, 커스텀 톤커브 | **SVG 필터** — 대안이 없다 |
| 애니메이션이 필요한 값 | CSS 단축 필터는 **보간된다**. SVG 필터 어트리뷰트는 SMIL/JS만 가능 |

**혼합 사용이 가능하다** — 무거운 SVG 필터는 정적으로 두고, 값이 변하는 부분만 CSS로 처리하는 것이 최적이다.

```css
.card {
  /* SVG 필터는 고정, CSS 필터만 트랜지션 */
  filter: url(#texture) brightness(1);
  transition: filter 240ms ease;
}
.card:hover {
  filter: url(#texture) brightness(1.15) saturate(1.2);
}
```
> `filter` 목록에서 `url()`은 그대로 두고 뒤의 CSS 함수만 바뀌면 브라우저가 그 부분만 보간한다.

**CSS 필터로 대체 가능한지 먼저 확인하라.**

| SVG 필터 조합 | CSS 대체 |
|---|---|
| `feColorMatrix type="saturate" values="0"` | `filter: grayscale(1)` (sRGB로 동작) |
| `feGaussianBlur` + `feOffset` + `feFlood` + `feMerge` | `filter: drop-shadow(x y blur color)` |
| `feColorMatrix type="hueRotate"` | `filter: hue-rotate(deg)` |
| 큰 소프트 글로우 | **`radialGradient` 또는 `box-shadow`가 압도적으로 싸다** |

---

## 3. `backdrop-filter` 함정

### 3.1 지원 현황

| 기능 | Chromium | Safari | Firefox |
|---|---|---|---|
| `backdrop-filter: blur()` 등 CSS 함수 | ✅ | ✅ (`-webkit-` 접두사 필요했던 시기 있음) | ✅ |
| **`backdrop-filter: url(#svgFilter)`** | ✅ | ❌ | ❌ |

`backdrop-filter`에 SVG 필터를 넣는 것은 **CSS 스펙에 없는 Chromium 확장**이다. Liquid Glass류 효과가 Chrome에서만 보이는 이유가 이것이다. Firefox에는 기능 요청이 접수되어 있고(2025), `mdn/browser-compat-data`에도 관련 이슈가 열려 있다.

### 3.2 기능 감지가 신뢰할 수 없다 ⚠️

```js
CSS.supports('backdrop-filter', 'url(#x)')          // Chromium: true (검증됨)
CSS.supports('backdrop-filter', 'url(#x) brightness(1.1)')  // Chromium: true (검증됨)
```

`CSS.supports`와 `@supports`는 **값을 파싱할 수 있는지**만 검사한다. Firefox/Safari도 `url()`을 유효한 `<filter-function>` 문법으로 파싱하므로 `true`를 반환할 가능성이 높다 — 그런데 렌더링은 하지 않는다.

**권장 전략: 감지하지 말고, 계단식으로 쌓아라.**

```css
/* 1단계: 모든 브라우저에서 그럴듯한 기본값 */
.glass {
  backdrop-filter: blur(14px) saturate(1.4);
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.28);
  box-shadow: inset 0 1px 1px rgba(255,255,255,.6), 0 10px 30px rgba(0,0,0,.4);
}

/* 2단계: 굴절을 지원하면 덮어쓴다.
   지원 안 하는 엔진에서는 이 선언이 무효 처리되어 1단계가 남는다. */
@supports (backdrop-filter: url(#lens)) {
  .glass { backdrop-filter: url(#lens) brightness(1.06) saturate(1.25); }
}
```
> `@supports`가 통과해버리는 엔진에서는 `backdrop-filter`가 통째로 무시되어 유리 배경만 남는다. 그래서 **`background`와 `box-shadow`만으로도 유리처럼 보이게** 설계해두는 것이 핵심이다.

**확실히 가르고 싶다면 렌더 프로브를 써라** (미검증 — 로직 참고용):

```js
async function supportsBackdropSVGFilter() {
  if (!('CSS' in window) || !CSS.supports('backdrop-filter', 'blur(1px)')) return false;
  // Chromium 계열만 확실히 지원한다는 사실에 기대는 실용적 판단
  const ua = navigator.userAgent;
  const isChromium = /Chrome|Chromium|Edg/.test(ua) && !/OPR|Firefox/.test(ua);
  const isAppleWebKit = /Safari/.test(ua) && !/Chrome|Chromium|Edg/.test(ua);
  return isChromium && !isAppleWebKit;
}
```
> UA 스니핑은 최후의 수단이다. 실패 시 폴백이 충분히 좋다면 감지 자체를 하지 않는 편이 낫다.

### 3.3 크기 불일치

`backdrop-filter`의 필터 영역은 요소 크기에 **자동으로 맞춰지지 않는다.** `feImage` 변위맵의 `width/height`가 요소와 다르면 굴절이 어긋나거나 잘린다.

- 크기별로 별도 맵을 만들어라.
- 반응형으로 크기가 변하는 요소에는 이 기법을 쓰지 마라.
- 크기 변경 애니메이션은 매 프레임 맵 재계산 = 최악의 시나리오.

### 3.4 `backdrop-filter`는 배경 합성을 강제한다

배경이 스크롤하거나 변하면 **매 프레임 배경을 다시 캡처해 필터를 돌린다.** 뷰포트 전체를 덮는 `backdrop-filter`는 스크롤 성능을 파괴한다. 작은 칩/툴바/모달 헤더에만 써라.

---

## 4. 좌표계·색공간 함정 (요약과 진단)

§01에 상세히 있고, 여기서는 **증상 → 원인** 진단표로 정리한다.

| 증상 | 원인 | 처방 |
|---|---|---|
| 효과가 사각형으로 잘림 | 필터 영역이 좁음 | `x/y/width/height` 확대 |
| 색이 물빠진 듯 밝고 채도가 낮음 | linearRGB 기본값 | `color-interpolation-filters="sRGB"` |
| 듀오톤 색이 지정한 hex와 다름 | 위와 동일 | 위와 동일 |
| `stdDeviation="10"`인데 화면이 사라짐 | `primitiveUnits="objectBoundingBox"` | 값을 0~1 스케일로 다시 계산 |
| 변위가 이상한 방향으로 감 | `xChannelSelector`/`yChannelSelector` 기본값이 `A` | `R`/`G` 명시 |
| 색이 전부 흰색으로 날아감 | `feColorMatrix` 값에 0~255를 씀 | 255로 나눠 0~1로 |
| 필터 영역 전체가 단색 사각형 | `feFlood`/조명 결과를 클리핑 안 함 | `feComposite operator="in"` + `SourceAlpha` |
| 분기 배선이 무시됨 | `in`을 명시하지 않아 직전 출력이 들어감 | 양쪽 모두 `in` 명시 |
| 노이즈가 컬러 스노우로 보임 | feTurbulence의 알파도 노이즈 | `feColorMatrix`로 A를 1 상수화 |
| 노이즈에 격자 사각형이 보임 | `seed`가 514/1977/2337/4777/8032/9615 등 | seed 변경 |
| 요소가 통째로 사라짐 | `url(#id)` 오타 (레거시 엔진) | ID 확인 |

---

## 5. CSS 레이아웃 부작용 (놓치기 쉬운 것)

`filter`가 `none`이 아닌 값을 가지면 그 요소는:

1. **새 스태킹 컨텍스트**를 만든다 → `z-index` 계층이 갇힌다.
2. **`position: fixed` / `absolute` 자손의 컨테이닝 블록이 된다** → 하위의 `position: fixed` 모달이 뷰포트가 아니라 그 요소 기준으로 배치된다.

`backdrop-filter`, `perspective`, `will-change: transform`, `contain: layout|paint|strict|content`, `content-visibility: auto`도 동일한 부작용을 갖는다.

> 이 동작은 호환성 때문에 표준화된 것이다. 모바일 WebKit이 먼저 하고 Chrome 22에서 따라갔으며, 2015년 워킹그룹 결의를 거쳐 Firefox가 마지막으로 합류했다.

**실무 영향**

```html
<!-- 카드에 질감 필터를 걸었더니, 카드 안의 드롭다운이 화면 밖으로 못 나간다 -->
<div class="card" style="filter: url(#texture)">
  <div class="dropdown" style="position: fixed">...</div>  <!-- 카드 기준으로 배치됨 -->
</div>
```

**회피**
- 필터를 콘텐츠 컨테이너가 아니라 **별도의 표면(surface) 레이어**에만 건다 (레시피 부록 B의 패턴).
- 오버레이/모달은 Portal로 `<body>` 직속에 렌더한다.
- DevTools는 "이 요소가 컨테이닝 블록을 만든다"를 시각적으로 표시해주지 않는다. 증상(모달 위치가 이상함)에서 역추적하는 수밖에 없다.

---

## 6. 브라우저별 알려진 이슈

### 6.1 Safari / WebKit

| 이슈 | 영향 |
|---|---|
| `backdrop-filter: url()` 미지원 | Liquid Glass 계열 전부 폴백 필요 |
| `feImage`로 **외부 SVG 파일** 참조 미지원 | data URI로 우회 |
| `feImage` **fragment 참조**(`href="#id"`)는 Safari **만** 지원 | 크로스브라우저에서는 쓸 수 없음 |
| `feImage`가 체인에 있으면 **HTML 요소에 필터 적용 실패** 보고 | SVG 콘텐츠에만 적용하거나 폴백 |
| 대형 요소에 필터를 걸면 고해상도에서 **아예 렌더되지 않는** 경우 보고 | 필터 적용 면적 제한 |
| SVG `<filter>`보다 CSS 단축 필터가 훨씬 안정적 | 가능하면 CSS 단축 사용 |
| `feGaussianBlur`의 `edgeMode`는 **Safari만 지원** | 크로스브라우저에서는 사용 불가 |

### 6.2 Firefox

| 이슈 | 영향 |
|---|---|
| `backdrop-filter: url()` 미지원 (기능 요청 계류 중) | 폴백 필요 |
| `feTurbulence` 성능 이슈 장기 보고 | 큰 면적 금지 |
| 하드웨어 가속 활성 시 필터 미동작 버그 보고 | 사용자 환경에 따라 효과가 안 보일 수 있음 |
| `feComposite operator="arithmetic"` 시각 아티팩트 버그 보고 | 대안 연산자 고려 |
| 블러 반경 상한(≈100px) | 큰 블러는 다른 방법으로 |
| `feGaussianBlur edgeMode` 미지원 | — |

### 6.3 모바일 전반

- 큰 면적 필터 + 애니메이션 = **배터리 소모 + 열 스로틀링**. 스로틀링이 걸리면 페이지 전체가 느려진다.
- 일부 안드로이드 GPU는 필터 가속을 전혀 못 한다.
- 저가 기기에서 `feTurbulence` 애니메이션은 사실상 사용 불가.
- GSAP 포럼 등에서 "데스크톱은 멀쩡한데 모바일에서 feTurbulence가 죽는다"는 사례가 반복 보고된다.

### 6.4 feTurbulence seed 아티팩트

특정 `seed` 값에서 노이즈에 **격자 사각형**이 생긴다. 보고된 값: `514, 1977, 2337, 4777, 8032, 9615` 등.

- 원인은 SVG 스펙의 참조 구현에 있는 정수 나눗셈 버그다. gradient 테이블의 두 값이 모두 0이 되는 경우가 생긴다.
- Chrome, Firefox, Batik, Inkscape 등 독립 구현들이 스펙을 그대로 따랐기 때문에 **모두 동일하게 재현**된다.
- **처방**: seed는 무작위로 생성하지 말고, 1~50 정도의 작은 값 중 눈으로 확인한 것을 하드코딩하라.

---

## 7. 접근성

### 7.1 `prefers-reduced-motion`

애니메이션되는 필터는 전정계 장애가 있는 사용자에게 실제로 불편을 준다. **모든 애니메이션 필터에 감소 모션 처리를 넣어라.**

```css
/* 방법 1: 필터 자체를 끈다 */
@media (prefers-reduced-motion: reduce) {
  .wave, .squiggle-art, .glitch-box { filter: none; }
}

/* 방법 2: 정적 버전으로 대체 */
@media (prefers-reduced-motion: reduce) {
  .pulse-card { filter: none; box-shadow: 0 0 0 3px #22d3ee; }
}
```

**SMIL은 CSS 미디어쿼리로 끌 수 없다.** JS로 처리해야 한다.

```js
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');

function applyMotionPreference() {
  const svgRoot = document.querySelector('#filterDefs');
  if (!svgRoot) return;
  if (mq.matches) {
    // SMIL 타임라인 정지
    if (typeof svgRoot.pauseAnimations === 'function') svgRoot.pauseAnimations();
  } else {
    if (typeof svgRoot.unpauseAnimations === 'function') svgRoot.unpauseAnimations();
  }
}

applyMotionPreference();
mq.addEventListener('change', applyMotionPreference);
```
> **미검증**: `pauseAnimations()`/`unpauseAnimations()`는 `SVGSVGElement`의 표준 메서드지만 이 세션에서 실측하지 않았다. SMIL을 아예 쓰지 않고 JS 타이머로 어트리뷰트를 바꾸는 방식(레시피 11)이 제어가 확실하다.

> ⚠️ SVG를 `<img>`나 CSS `background-image`로 넣으면 브라우저가 샌드박스 처리해 **내부의 `prefers-reduced-motion` 미디어쿼리가 평가되지 않는다.** 인라인 SVG를 쓰거나, 애니메이션을 바깥 CSS에서 제어하라.

### 7.2 텍스트 가독성

- 왜곡 필터를 텍스트에 걸면 **판독성이 떨어진다.** `feDisplacementMap scale`은 텍스트에서 4~8을 넘기지 마라.
- 필터는 **DOM 텍스트를 그대로 유지**한다 — 선택·복사·검색·스크린리더 접근은 살아있다. 이것이 이미지로 굽는 것보다 나은 결정적 이유다.
- 듀오톤/포스터화로 **대비가 낮아질 수 있다.** 텍스트 위에 적용한다면 WCAG 대비비를 실제 렌더 결과로 다시 측정하라.
- 색수차(RGB split)는 난독증·시각 피로가 있는 사용자에게 특히 읽기 어렵다. 본문 텍스트에는 쓰지 마라.

### 7.3 필터 정의 SVG의 접근성

```html
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <defs><filter id="...">...</filter></defs>
</svg>
```
- `aria-hidden="true"`: 스크린리더가 빈 그래픽으로 읽지 않게.
- `focusable="false"`: 구형 IE/Edge에서 탭 순서에 들어가지 않게.
- `width="0" height="0"` + `position:absolute`: 레이아웃에 영향 없음.

---

## 8. 폴백 · 저사양 대응 전략

### 8.1 계층적 설계 (권장)

```
Layer 0 — 필터 없이도 완성된 디자인 (색, 그림자, 그라디언트)
Layer 1 — CSS 단축 필터로 얹는 개선 (blur, saturate, drop-shadow)
Layer 2 — SVG 필터로 얹는 개선 (질감, 굴절, 색수차)
```

각 레이어가 없어도 아래 레이어가 완결되어야 한다. **필터를 지웠을 때 깨지는 디자인은 잘못 설계된 것이다.**

### 8.2 저사양 기기 감지

```js
function shouldDisableHeavyFilters() {
  // 1) 사용자 선호
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return true;

  // 2) 하드웨어 힌트 (모든 브라우저가 제공하지는 않음)
  const cores = navigator.hardwareConcurrency;      // Chromium/Firefox
  const memory = navigator.deviceMemory;            // Chromium only, GB 단위
  if (typeof cores === 'number' && cores <= 4) return true;
  if (typeof memory === 'number' && memory <= 4) return true;

  // 3) 네트워크 절약 모드 (Chromium)
  const conn = navigator.connection;
  if (conn && conn.saveData) return true;

  // 4) 좁은 화면 = 대체로 모바일
  if (window.matchMedia('(max-width: 640px)').matches) return true;

  return false;
}

if (shouldDisableHeavyFilters()) {
  document.documentElement.classList.add('reduce-effects');
}
```

```css
/* 무거운 필터를 값싼 대체물로 교체 */
.reduce-effects .paper-surface { filter: none; background: #e8e0d0; }
.reduce-effects .squiggle-art  { filter: none; }
.reduce-effects .prism-pill    { backdrop-filter: blur(10px) saturate(1.4); }
.reduce-effects .grainy::after { opacity: 0.25; }  /* 그레인은 싸니 약하게 유지 */
```

> `navigator.hardwareConcurrency`/`deviceMemory`는 Chromium에서 실측 확인했다(각각 24, 32 반환). Safari는 `deviceMemory`를 제공하지 않으므로 `typeof` 가드가 필수다.

### 8.3 뷰포트 밖 / 백그라운드 탭에서 정지

애니메이션 필터에는 필수다.

```js
function gateAnimation(targetEl, start, stop) {
  let visible = false;
  let focused = !document.hidden;

  function sync() { (visible && focused) ? start() : stop(); }

  if ('IntersectionObserver' in window) {
    new IntersectionObserver(entries => {
      visible = entries[0].isIntersecting;
      sync();
    }, { threshold: 0 }).observe(targetEl);
  } else {
    visible = true;
  }

  document.addEventListener('visibilitychange', () => {
    focused = !document.hidden;
    sync();
  });

  sync();
}
```

### 8.4 "구워서 쓰기" — 가장 확실한 최적화

정적 텍스처(종이 질감, 그레인 패턴, 리소 그레인)는 **한 번 렌더해서 PNG/WebP로 저장**하고 `background-image`로 쓰는 것이 압도적으로 싸다.

- 필터 체인이 아무리 길어도 런타임 비용은 0이 된다.
- 해상도 독립성을 잃지만, 노이즈/텍스처는 애초에 해상도 독립성이 필요 없다.
- 512×512 타일을 만들어 `background-repeat`로 깔면 파일도 작다.
- CSS-Tricks의 패턴 생성 글에서도 복잡한 필터 패턴은 "JPEG/PNG로 서빙해 사용자 CPU를 아끼라"고 권한다.

**data URI vs 요소 필터** — 이미 §02 레시피 02/03에서 쓴 방식이다. `background-image: url("data:image/svg+xml,...")`는 **한 번만 래스터화되어 캐시**되므로, 같은 노이즈를 `filter: url(#noise)`로 거는 것보다 훨씬 싸다. **노이즈는 언제나 data URI 배경으로.**

---

## 9. 측정과 디버깅

### 9.1 Chrome DevTools

1. **Performance 패널** → 녹화 → "Rendering" 트랙에서 Paint/Composite 시간 확인. 필터가 문제면 Paint가 길어진다.
2. **Rendering 탭** → "Paint flashing" 켜기 → 필터 영역이 매 프레임 초록색으로 깜빡이면 재계산 중이다.
3. **Rendering 탭** → "Layer borders"로 합성 레이어 승격 여부 확인.
4. **CPU throttling 4×/6×** 로 저사양 기기 시뮬레이션. 이것이 가장 현실적인 테스트다.

### 9.2 A/B 비교 방법

필터 하나가 얼마나 비싼지 알고 싶으면 DevTools 콘솔에서 껐다 켜본다.

```js
document.querySelectorAll('.effect').forEach(el => { el.style.filter = 'none'; });
```
FPS가 확 오르면 그 필터가 범인이다.

### 9.3 오프라인 도구

| 도구 | 용도 |
|---|---|
| [yoksel/svg-filters](https://yoksel.github.io/svg-filters/) | 필터 체인을 GUI로 만들고 코드 추출 |
| [yoksel/svg-gradient-map](https://yoksel.github.io/svg-gradient-map/) | 듀오톤/트라이톤 `tableValues` 자동 생성 |
| [fffuel nnnoise](https://www.fffuel.co/nnnoise/) | 노이즈 텍스처 SVG 생성 |

---

## 10. 프로덕션 체크리스트

배포 전에 이 목록을 통과시켜라.

**성능**
- [ ] 필터 영역(`x/y/width/height`)이 필요한 최소 크기인가
- [ ] `numOctaves`가 4를 넘지 않는가
- [ ] 애니메이션되는 필터의 적용 면적이 400×400px 이하인가
- [ ] 애니메이션 필터에 IntersectionObserver / visibilitychange 게이팅이 있는가
- [ ] 필터와 `transform` 애니메이션이 같은 요소에 걸려 있지 않은가
- [ ] `will-change: filter`를 남발하지 않았는가
- [ ] 정적 텍스처를 런타임 필터로 매번 계산하고 있지 않은가 (→ data URI 배경 또는 PNG)
- [ ] CPU throttling 4×에서 스크롤이 부드러운가

**호환성**
- [ ] `color-interpolation-filters`를 의도적으로 설정했는가
- [ ] `feDisplacementMap`의 채널 셀렉터를 명시했는가
- [ ] `feImage`를 쓴다면 data URI인가 (외부 파일/fragment 아님)
- [ ] `backdrop-filter: url()`에 의존한다면 폴백만으로도 완성도가 있는가
- [ ] `edgeMode`, `kernelUnitLength`, `BackgroundImage`, `FillPaint`를 쓰지 않았는가
- [ ] `feTurbulence` seed가 알려진 문제 값이 아닌가
- [ ] Safari / Firefox / 모바일 실기기에서 눈으로 확인했는가

**접근성**
- [ ] 애니메이션 필터에 `prefers-reduced-motion` 처리가 있는가
- [ ] 필터 정의 SVG에 `aria-hidden="true"`가 있는가
- [ ] 텍스트에 필터를 걸었다면 판독성과 대비비를 확인했는가
- [ ] 필터가 전부 꺼져도 디자인이 성립하는가

**레이아웃**
- [ ] 필터를 건 요소 안에 `position: fixed` 자손이 없는가
- [ ] 필터로 인한 스태킹 컨텍스트가 `z-index` 계층을 깨지 않는가
- [ ] 필터 컨테이너 안에 본문 텍스트가 들어가 있지 않은가
