Repository navigation
perf: 배포 용량 경량화 — sharp 제거로 50MB → 19MB - #7
Merged
Merged
Conversation
Cut the npx install from 50MB to 19MB. sharp's libvips binaries were 27MB of that, and the @img/sharp-wasm32 half of it was never loaded. Blocking that one package is not possible from here: npm overrides only apply at the root project, so they never reach an end user's install. Removing sharp does. @jsquash ships the PNG/JPEG/WebP codecs as wasm in 1.8MB. It has no resize or composite, but the pipeline only ever asked sharp for nearest-neighbour scaling and RGBA blitting, both of which already existed here in pure JS. Decode output was compared against sharp before removing it: PNG and WebP byte-identical, JPEG within the mozjpeg/libjpeg-turbo DCT rounding margin, resize and GIF exact. Also drop three things the archive never needed: sourcemaps (kept in dev builds), docs/media screenshots, and the 92 bundled Pretendard subsets, now loaded from a CDN behind a system-font fallback stack.
prepare-package.mjs copied web/, docs/ and skills/ with `force: true`, which overwrites colliding files but never removes ones that are no longer produced. Anything left in packages/pixel-cli/docs/ from an earlier build therefore survived the docs/media filter entirely and was picked up by `files: ["docs"]` at pack time — invisible to git, since that path is gitignored. Windows CI caught it as a stray package/docs/media/ in the tarball. postpack --clean does run, but only after the tarball is sealed, so it cannot prevent a leftover from shipping. Clean the destinations before copying instead, which makes the build idempotent regardless of whether postpack ever ran. The new test plants leftovers in a fixture checkout and asserts the build removes them, covering the output rather than the filter source text that the existing test checks.
The docs filter compared `relative(repositoryRoot, source)` against `docs/media`, which only holds while both sides spell the directory the same way. They need not: `import.meta.url` is realpath-normalized by the ESM loader, while the path handed to `fs.cp` is whatever the caller wrote. Under a junction, a subst drive, an 8.3 short name, or a symlink, `relative()` returns a `..`-prefixed path that matches no exclusion, and the filter silently copies `docs/media` into the archive. Canonicalize both sides with realpathSync.native, and reject anything resolving outside the repository root rather than publishing it. The same class of bug was fixed for the CLI entrypoint check in af48eae. The win32 regression runs on every platform, since path.win32 makes the comparison a pure function.
NariP
added a commit
that referenced
this pull request
Sep 4, 2026
* perf: replace sharp with wasm codecs and slim the published package Cut the npx install from 50MB to 19MB. sharp's libvips binaries were 27MB of that, and the @img/sharp-wasm32 half of it was never loaded. Blocking that one package is not possible from here: npm overrides only apply at the root project, so they never reach an end user's install. Removing sharp does. @jsquash ships the PNG/JPEG/WebP codecs as wasm in 1.8MB. It has no resize or composite, but the pipeline only ever asked sharp for nearest-neighbour scaling and RGBA blitting, both of which already existed here in pure JS. Decode output was compared against sharp before removing it: PNG and WebP byte-identical, JPEG within the mozjpeg/libjpeg-turbo DCT rounding margin, resize and GIF exact. Also drop three things the archive never needed: sourcemaps (kept in dev builds), docs/media screenshots, and the 92 bundled Pretendard subsets, now loaded from a CDN behind a system-font fallback stack. * fix: rebuild the package staging tree instead of merging into it prepare-package.mjs copied web/, docs/ and skills/ with `force: true`, which overwrites colliding files but never removes ones that are no longer produced. Anything left in packages/pixel-cli/docs/ from an earlier build therefore survived the docs/media filter entirely and was picked up by `files: ["docs"]` at pack time — invisible to git, since that path is gitignored. Windows CI caught it as a stray package/docs/media/ in the tarball. postpack --clean does run, but only after the tarball is sealed, so it cannot prevent a leftover from shipping. Clean the destinations before copying instead, which makes the build idempotent regardless of whether postpack ever ran. The new test plants leftovers in a fixture checkout and asserts the build removes them, covering the output rather than the filter source text that the existing test checks. * fix: compare packaging paths by canonical form, and fail closed The docs filter compared `relative(repositoryRoot, source)` against `docs/media`, which only holds while both sides spell the directory the same way. They need not: `import.meta.url` is realpath-normalized by the ESM loader, while the path handed to `fs.cp` is whatever the caller wrote. Under a junction, a subst drive, an 8.3 short name, or a symlink, `relative()` returns a `..`-prefixed path that matches no exclusion, and the filter silently copies `docs/media` into the archive. Canonicalize both sides with realpathSync.native, and reject anything resolving outside the repository root rather than publishing it. The same class of bug was fixed for the CLI entrypoint check in 37cb61f. The win32 regression runs on every platform, since path.win32 makes the comparison a pure function.
This was referenced Sep 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
무엇이 바뀌나
npx로 설치되는 용량이 50MB → 19MB (-62%) 로 줄었다. 첫 실행 시 다운로드가 그만큼 빨라진다.가장 큰 변화는 sharp 를 걷어내고
@jsquashwasm 코덱으로 교체한 것이다. 이미지 변환·렌더링·내보내기 동작은 동일하고, 출력 픽셀도 그대로다.배경
Closes #6플러그인 마켓플레이스 배포(#4)로
npx -y -p editable-pixel@latest editable-pixel-mcp경로가 열리면서 첫 실행 다운로드 용량이 사용자 체감에 직접 영향을 주게 됐다. 50MB 내역을 실측한 결과 sharp 계열이 27MB 로 절반이었다.당초 계획이었던
@img/sharp-wasm32만 차단하는 방식은 불가능으로 판명났다. npmoverrides는 루트 프로젝트에서만 유효하고 하위 소비자로 전파되지 않는다 — tarball 로 패킹해 하위 설치하면 8.7MB 가 그대로 복귀하는 것을 실측으로 확인했다. 우리package.json에 넣어도 고친 것처럼 보이는 no-op 이 될 뿐이다. 그래서 sharp 자체를 제거하는 방향으로 전환했다 (이슈 코멘트에 상세 기록).작업 내용
sharp → @jsquash 교체
packages/pixel-image-codec/— 신규 워크스페이스 패키지. wasm 코덱 래핑(decodeImage/encodePng), 매직바이트 포맷 판별,resizeNearest,compositeRgba/blitRgbapackages/pixel-renderer/src/node.ts:40-105— sharp 호출 6곳 교체packages/pixel-converter/src/index.ts:166,229— sharp 호출 6곳 교체, 기존blitRgba를 공용 유틸로 승격packages/pixel-renderer/src/gif-metadata.ts,tests/support/gif.ts—sharp(gif,{animated:true})대체 GIF 리더pnpm-workspace.yaml에서 제거후보 조사는 전부 실제 설치·실행으로 검증했다.
jimp은 npm 메타데이터상 3.3MB 지만 실측 31MB 로 sharp 보다 크고 WebP 도 안 되며,@napi-rs/canvas도 실측 26MB 로 이득이 없었다. JPEG/WebP 디코드를 만족하면서 유의미하게 작은 건@jsquash(1.8MB) 뿐이었다.배포 설정 정리
packages/pixel-cli/package.json:25—files의"dist"→"dist/*.js". 소스맵 1.9MB 제외(개발 빌드는 유지)packages/pixel-cli/scripts/prepare-package.mjs:23-38—docs/media616KB 제외.docs/*.md는 유지apps/web/index.html:8-12— Pretendard 를 jsDelivr CDN 으로 전환(3.0MB 제외) +preconnectapps/web/src/styles.css:6-7— 시스템 UI 폰트 폴백 스택 보강측정
du -sh node_modules)npm packunpackednpm pack파일 수apps/web/dist남는 19MB 중
@modelcontextprotocol7.5MB +zod6.3MB 는 손댈 수 없다. zod 는 MCP SDK 가dependencies로 하드 의존하고 내부에서from "zod/v4"로 직접 import 하므로, 우리가 걷어내도 SDK 가 동일 패키지를 끌어와 절감 0 이다.자가체크
루프 — 4회차에서 APPROVE.
코드 리뷰 — 위반 2건 지적, 전부 수정:
tests/distribution/slim-package.test.ts의 bin 검증이 자기가 만든 문자열을 자기가 검사하는 tautology 였다.Object.entries(manifest.bin)순회로 교체하고splitting: false단언을 추가했다.blitRgba에 입력 길이 검증이 없어, 짧은 버퍼가 들어오면subarray가 짧은 슬라이스를 반환해 조용히 절반만 그려졌다. sharp 의.composite()는 예외를 던졌으므로 회귀였다. 진입부 검증 +RangeError로 수정.개선 3건(init 실패 영구 캐시, 디코드 에러 진단 정보 부재,
prepare-package.mjs의 부분문자열 매칭)도 반영했다.테스트 —
pnpm verifyexit 0, 251 tests.pnpm test:e2e23/23 통과.mutation check 로 각 수정이 실제로 잡는지 확인했다:
manifest.bin→./dist/sub/cli.jssplitting: truerelative()→ substring 환원once()→ rejection 캐시QA 감사 — pass. 골든값이 sharp 시절 바이트임을 구조로 확인했다(
SHARP_PNG에pHYs청크가 있는데 우리 인코더는 이를 emit 하지 않으므로, 자기 출력을 자기가 승인한 것이 아님). resize 골든에 축소 2건·비등방 1건이 포함돼 중심 샘플링 공식 오류를 잡을 수 있다는 점도 확인됐다.제거한 테스트 — 없음. 기존 기대값도 수정하지 않았다.
폐기한 완료 기준 1건 —
PR self-check: pnpm install 후 @img/sharp-wasm32 가 없고...는 pnpm 이 부모의os/cpu를 존중해 main 에서도 이미 참이었다. 변경 전후가 동일한 기준은 아무것도 증명하지 못한다.package-install.mjs:92의assert.rejects(access(@img), ENOENT)로 대체됐다.QA 시나리오
이미지 파이프라인이 교체됐으므로 실제 변환·렌더링 경로를 확인해달라.
editable-pixel convert <파일>.png --size 32→ Pixel Document 가 생성되고 색상이 원본과 맞는지.jpg,.webp로. 특히 WebP 는 이번에 디코더가 바뀐 포맷이다editable-pixel render <파일>.pixel.json --scale 4→ 정수 배율에서 픽셀이 뭉개지지 않는지실제 배포본으로 1·3번은 이미 확인했다 — pack → 격리 디렉터리 설치 → 실행으로 convert(PNG 디코드)와 render(32x32 PNG 출력) 정상 동작, sharp/@img 부재, wasm 6개 포함을 확인했다.
알려진 갭
thresholdAlpha가 resize 전에 알파를 0/255 로 이진화하므로 도달 불가능하고, 코드 리뷰와 QA 가 각각 코드 추적으로 이를 확인했다. 다만 불변조건이 주석으로만 존재해, 향후 리팩터링이 thresholding 을 뒤로 옮기면 조용히 깨진다. 후속 이슈로 테스트를 추가할 만하다.docs/media필터 테스트가 스크립트를 import 하지 않고 로직을 미러링한다.prepare-package.mjs가 top-level 에서 복사를 실행해 import 시 빌드가 돌기 때문이다. 스크립트에relative(repositoryRoot, source)가 있는지 단언해 고정했고,package-install.mjs가 실제 tar 목록으로 한 번 더 검증한다.blitRgba의 검증 추가는 공개 export 의 계약을 조인다. 외부 소비자가 있다면 동작 변경이다. 저장소 내 두 호출자는 모두 안전함을 확인했다.리뷰 포인트
packages/pixel-image-codec/를 별도 패키지로 분리한 구조가 적절한가후속 이슈 후보
prepare-package.mjs의 필터를 side-effect 없는 모듈로 분리해 테스트가 직접 import 하도록@modelcontextprotocol의 소스맵·미사용 서브패키지(ajv, cfWorker, shims) 업스트림 리포트🤖 auto-generated