designpaca/research/svg/03-performance-and-pitfalls.md
Yun Chan 8808c672dc designpaca 초기 구현 — 스킬 · 설치 CLI · 배포 파이프라인
웹 디자인 파이프라인 스킬과 이를 5개 에이전트에 설치하는 CLI 를 담은 모노레포.

스킬 (packages/skill)
- SKILL.md 261줄 + 참조 문서 16개 3,349줄. progressive disclosure 로
  본문은 절차와 인덱스만, 지식은 references/ 로 분리
- 0~6단계 파이프라인. 규모에 따라 전체·연장·국소 세 경로로 분기
- 하드 게이트 12개는 grep·카운트로 검증 가능한 것만. 취향 판단은 제외
- 미학 프리셋 5종, AI 슬롭 지문 목록, 한글 조판 규칙,
  SVG 필터·three.js·인터랙티브 모션·HTML-in-Canvas 실전 지침

설치 CLI (packages/cli, packages/core)
- npx designpaca 온보딩 TUI. Claude Code · Codex · Cursor · Windsurf · AGENTS.md
- 매니페스트에 설치 시점 해시를 기록해 사용자가 고친 파일은 update 가 건너뛴다
- 타깃별로 본문의 references/ 경로를 실제 설치 위치로 재작성
- AGENTS.md 는 항상 로드되므로 본문 대신 303자 포인터만 주입
- Windsurf 는 12,000자 상한 초과 시 설치를 차단

배포 (build/ci, .forgejo/workflows)
- 태그 v* → 검사·테스트·빌드 → npmjs 배포 + Forgejo 레지스트리 미러
  → draft 릴리스 → Cloudflare Pages. 재실행 멱등

근거 (research/)
- 약 250개 웹 소스 조사 결과와 도그푸딩 검증 2건. 스킬의 모든 수치는 여기서 나온다

테스트 22개 통과 (core 16 · cli 6)
2026-08-20 10:48:00 +09:00

24 KiB
Raw Blame History

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으로 움직이는 것. 브라우저가 캐시된 레이어를 합성하는 대신 매 프레임 필터 전체를 다시 돌릴 수 있다. 필터와 모션은 서로 다른 요소에 분리하라.

<!-- ❌ 같은 요소에 필터 + 모션 -->
<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로 붙였다가 끝나면 제거하는 패턴은 유효하다.
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로 처리하는 것이 최적이다.

.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 기능 감지가 신뢰할 수 없다 ⚠️

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를 반환할 가능성이 높다 — 그런데 렌더링은 하지 않는다.

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

/* 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가 통째로 무시되어 유리 배경만 남는다. 그래서 backgroundbox-shadow만으로도 유리처럼 보이게 설계해두는 것이 핵심이다.

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

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 레이아웃 부작용 (놓치기 쉬운 것)

filternone이 아닌 값을 가지면 그 요소는:

  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가 마지막으로 합류했다.

실무 영향

<!-- 카드에 질감 필터를 걸었더니, 카드 안의 드롭다운이 화면 밖으로 못 나간다 -->
<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 단축 사용
feGaussianBluredgeModeSafari만 지원 크로스브라우저에서는 사용 불가

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

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

/* 방법 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로 처리해야 한다.

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의 접근성

<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 저사양 기기 감지

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');
}
/* 무거운 필터를 값싼 대체물로 교체 */
.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 뷰포트 밖 / 백그라운드 탭에서 정지

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

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 콘솔에서 껐다 켜본다.

document.querySelectorAll('.effect').forEach(el => { el.style.filter = 'none'; });

FPS가 확 오르면 그 필터가 범인이다.

9.3 오프라인 도구

도구 용도
yoksel/svg-filters 필터 체인을 GUI로 만들고 코드 추출
yoksel/svg-gradient-map 듀오톤/트라이톤 tableValues 자동 생성
fffuel 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 계층을 깨지 않는가
  • 필터 컨테이너 안에 본문 텍스트가 들어가 있지 않은가