Yuki Takei 1 месяц назад
Родитель
Сommit
97c889da17

+ 324 - 0
.kiro/specs/optimize-presentation/design.md

@@ -0,0 +1,324 @@
+# Design Document: optimize-presentation
+
+## Overview
+
+**Purpose**: This feature decouples heavy Marp rendering dependencies (`@marp-team/marp-core`, `@marp-team/marpit`) from the common slide rendering path, so they are loaded only when a page explicitly uses `marp: true` frontmatter.
+
+**Users**: All GROWI users benefit from reduced JavaScript payload when viewing slide pages that do not use Marp. Developers benefit from clearer module boundaries.
+
+**Impact**: Changes the `@growi/presentation` package's internal import structure. No external API or behavioral changes.
+
+### Goals
+- Eliminate `@marp-team/marp-core` and `@marp-team/marpit` from the GrowiSlides module graph
+- Load MarpSlides and its Marp dependencies only on demand via dynamic import
+- Maintain identical rendering behavior for all slide types
+
+### Non-Goals
+- Optimizing the `useSlidesByFrontmatter` hook (already lightweight with internal dynamic imports)
+- Changing the app-level lazy-loading architecture (`useLazyLoader`, `next/dynamic` for the modal)
+- Modifying the Marp rendering logic or configuration itself
+- Reducing the size of `@marp-team/marp-core` internals
+
+## Architecture
+
+### Existing Architecture Analysis
+
+Current module dependency graph for `Slides.tsx`:
+
+```mermaid
+graph TD
+  Slides[Slides.tsx]
+  MarpSlides[MarpSlides.tsx]
+  GrowiSlides[GrowiSlides.tsx]
+  GrowiMarpit[growi-marpit.ts]
+  MarpCore[marp-team/marp-core]
+  Marpit[marp-team/marpit]
+
+  Slides --> MarpSlides
+  Slides --> GrowiSlides
+  MarpSlides --> GrowiMarpit
+  GrowiSlides --> GrowiMarpit
+  GrowiMarpit --> MarpCore
+  GrowiMarpit --> Marpit
+```
+
+**Problem**: Both `MarpSlides` and `GrowiSlides` statically import `growi-marpit.ts`, which instantiates Marp at module scope. This forces ~896KB of Marp modules to load even when rendering non-Marp slides.
+
+**Constraints**:
+- `growi-marpit.ts` exports both runtime Marp instances and a simple string constant (`MARP_CONTAINER_CLASS_NAME`)
+- `GrowiSlides` uses Marp only for CSS extraction (`marpit.render('')`), not for content rendering
+- The package uses Vite with `preserveModules: true` and `nodeExternals({ devDeps: true })`
+
+### Architecture Pattern & Boundary Map
+
+Target module dependency graph:
+
+```mermaid
+graph TD
+  Slides[Slides.tsx]
+  MarpSlides[MarpSlides.tsx]
+  GrowiSlides[GrowiSlides.tsx]
+  GrowiMarpit[growi-marpit.ts]
+  MarpCore[marp-team/marp-core]
+  Marpit[marp-team/marpit]
+  Consts[consts/index.ts]
+  BaseCss[consts/marpit-base-css.ts]
+
+  Slides -.->|React.lazy| MarpSlides
+  Slides --> GrowiSlides
+  MarpSlides --> GrowiMarpit
+  GrowiMarpit --> MarpCore
+  GrowiMarpit --> Marpit
+  GrowiMarpit --> Consts
+  GrowiSlides --> Consts
+  GrowiSlides --> BaseCss
+```
+
+**Key changes**:
+- Dashed arrow = dynamic import (React.lazy), solid arrow = static import
+- `GrowiSlides` no longer imports `growi-marpit.ts`; uses pre-extracted CSS from `marpit-base-css.ts`
+- `MARP_CONTAINER_CLASS_NAME` moves to shared `consts/index.ts`
+- Marp modules are only reachable through the dynamic `MarpSlides` path
+
+**Steering compliance**:
+- Follows "subpath imports over barrel imports" principle
+- Aligns with `next/dynamic({ ssr: false })` technique from build-optimization skill
+- Preserves existing lazy-loading boundaries at app level
+
+### Technology Stack
+
+| Layer | Choice / Version | Role in Feature | Notes |
+|-------|------------------|-----------------|-------|
+| Frontend | React 18 (`React.lazy`, `Suspense`) | Dynamic import boundary for MarpSlides | Standard React code-splitting; SSR not a concern (see research.md) |
+| Build | Vite (existing) | Package compilation with `preserveModules` | Dynamic `import()` preserved in output |
+| Build script | Node.js ESM (.mjs) | CSS extraction at build time | No additional tooling dependency |
+
+## System Flows
+
+### Slide Rendering Decision Flow
+
+```mermaid
+flowchart TD
+  A[Slides component receives props] --> B{hasMarpFlag?}
+  B -->|true| C[React.lazy loads MarpSlides chunk]
+  C --> D[MarpSlides renders via marp-core]
+  B -->|false| E[GrowiSlides renders with pre-extracted CSS]
+  E --> F[No Marp modules loaded]
+```
+
+### Build-Time CSS Extraction Flow
+
+```mermaid
+flowchart TD
+  A[pnpm run build] --> B[pre:build:src script runs]
+  B --> C[extract-marpit-css.mjs executes]
+  C --> D[Instantiate Marp with config]
+  D --> E[Call slideMarpit.render and presentationMarpit.render]
+  E --> F[Write marpit-base-css.ts with CSS constants]
+  F --> G[vite build compiles all sources]
+```
+
+## Requirements Traceability
+
+| Requirement | Summary | Components | Interfaces | Flows |
+|-------------|---------|------------|------------|-------|
+| 1.1 | No marp-core references in GrowiSlides build output | GrowiSlides, marpit-base-css | — | — |
+| 1.2 | Non-Marp slides render without loading Marp | Slides, GrowiSlides | — | Slide Rendering Decision |
+| 1.3 | Pre-extracted CSS constants for container styling | marpit-base-css | MarpitBaseCss | CSS Extraction |
+| 1.4 | MARP_CONTAINER_CLASS_NAME in shared consts | consts/index.ts | — | — |
+| 2.1 | Dynamic load MarpSlides for marp:true pages | Slides | — | Slide Rendering Decision |
+| 2.2 | Loading indicator during MarpSlides load | Slides | — | Slide Rendering Decision |
+| 2.3 | No MarpSlides import triggered for non-Marp | Slides | — | Slide Rendering Decision |
+| 3.1 | Build-time CSS extraction script | extract-marpit-css.mjs | ExtractScript | CSS Extraction |
+| 3.2 | Extraction runs before source compilation | package.json pre:build:src | — | CSS Extraction |
+| 3.3 | Generated file committed for dev mode | marpit-base-css.ts | — | — |
+| 4.1–4.5 | Functional equivalence across all render paths | All components | — | Both flows |
+| 5.1–5.4 | Build verification of module separation | Build outputs | — | — |
+
+## Components and Interfaces
+
+| Component | Domain | Intent | Req Coverage | Key Dependencies | Contracts |
+|-----------|--------|--------|--------------|------------------|-----------|
+| Slides | UI / Routing | Route between MarpSlides and GrowiSlides based on hasMarpFlag | 2.1, 2.2, 2.3 | MarpSlides (P1, dynamic), GrowiSlides (P0, static) | — |
+| GrowiSlides | UI / Rendering | Render non-Marp slides with pre-extracted CSS | 1.1, 1.2, 1.3, 1.4 | consts (P0), marpit-base-css (P0) | — |
+| marpit-base-css | Constants | Provide pre-extracted Marp theme CSS | 1.3, 3.3 | — | State |
+| consts/index.ts | Constants | Shared constants including MARP_CONTAINER_CLASS_NAME | 1.4 | — | — |
+| growi-marpit.ts | Service | Marp engine setup (unchanged, now only reached via MarpSlides) | 4.1, 4.3 | marp-core (P0, external), marpit (P0, external) | Service |
+| extract-marpit-css.mjs | Build Script | Generate marpit-base-css.ts at build time | 3.1, 3.2 | marp-core (P0, external), marpit (P0, external) | — |
+
+### UI / Routing Layer
+
+#### Slides
+
+| Field | Detail |
+|-------|--------|
+| Intent | Route rendering to MarpSlides (dynamic) or GrowiSlides (static) based on `hasMarpFlag` prop |
+| Requirements | 2.1, 2.2, 2.3 |
+
+**Responsibilities & Constraints**
+- Conditionally render MarpSlides or GrowiSlides based on `hasMarpFlag`
+- Wrap MarpSlides in `<Suspense>` with a loading fallback
+- Must not statically import MarpSlides
+
+**Dependencies**
+- Outbound: MarpSlides — dynamic import for Marp rendering (P1)
+- Outbound: GrowiSlides — static import for non-Marp rendering (P0)
+
+**Contracts**: State [x]
+
+##### State Management
+
+```typescript
+// Updated Slides component signature (unchanged externally)
+type SlidesProps = {
+  options: PresentationOptions;
+  children?: string;
+  hasMarpFlag?: boolean;
+  presentation?: boolean;
+};
+
+// Internal: MarpSlides loaded via React.lazy
+// const MarpSlides = lazy(() => import('./MarpSlides').then(mod => ({ default: mod.MarpSlides })))
+```
+
+- Persistence: None (stateless component)
+- Concurrency: Single render path per props
+
+**Implementation Notes**
+- Integration: Replace static `import { MarpSlides }` with `React.lazy` dynamic import
+- Validation: `hasMarpFlag` is optional boolean; undefined/false → GrowiSlides path
+- Risks: Suspense fallback visible during first MarpSlides load (mitigated by parent-level loading indicators)
+
+### UI / Rendering Layer
+
+#### GrowiSlides
+
+| Field | Detail |
+|-------|--------|
+| Intent | Render non-Marp slides using pre-extracted CSS instead of runtime Marp |
+| Requirements | 1.1, 1.2, 1.3, 1.4 |
+
+**Responsibilities & Constraints**
+- Render slide content via ReactMarkdown with section extraction
+- Apply Marp container CSS from pre-extracted constants (no runtime Marp dependency)
+- Use `MARP_CONTAINER_CLASS_NAME` from shared constants module
+
+**Dependencies**
+- Inbound: Slides — rendering delegation (P0)
+- Outbound: consts/index.ts — `MARP_CONTAINER_CLASS_NAME` (P0)
+- Outbound: consts/marpit-base-css — `SLIDE_MARPIT_CSS`, `PRESENTATION_MARPIT_CSS` (P0)
+
+**Implementation Notes**
+- Integration: Replace `import { ... } from '../services/growi-marpit'` with imports from `../consts` and `../consts/marpit-base-css`
+- Replace `const { css } = marpit.render('')` with `const css = presentation ? PRESENTATION_MARPIT_CSS : SLIDE_MARPIT_CSS`
+
+### Constants Layer
+
+#### marpit-base-css
+
+| Field | Detail |
+|-------|--------|
+| Intent | Provide pre-extracted Marp theme CSS as string constants |
+| Requirements | 1.3, 3.3 |
+
+**Contracts**: State [x]
+
+##### State Management
+
+```typescript
+// Generated file — do not edit manually
+// Regenerate with: node scripts/extract-marpit-css.mjs
+
+export const SLIDE_MARPIT_CSS: string;
+export const PRESENTATION_MARPIT_CSS: string;
+```
+
+- Persistence: Committed to repository; regenerated at build time
+- Consistency: Deterministic output from Marp config; changes only on `@marp-team/marp-core` version update
+
+#### consts/index.ts
+
+| Field | Detail |
+|-------|--------|
+| Intent | Shared constants for presentation package |
+| Requirements | 1.4 |
+
+**Implementation Notes**
+- Add `export const MARP_CONTAINER_CLASS_NAME = 'marpit'` (moved from `growi-marpit.ts`)
+- Existing `PresentationOptions` type export unchanged
+
+### Service Layer
+
+#### growi-marpit.ts
+
+| Field | Detail |
+|-------|--------|
+| Intent | Marp engine setup and instance creation (unchanged behavior, now only reachable via MarpSlides dynamic path) |
+| Requirements | 4.1, 4.3 |
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+// Existing exports (unchanged)
+export const MARP_CONTAINER_CLASS_NAME: string; // Re-exported from consts for backward compat
+export const slideMarpit: Marp;
+export const presentationMarpit: Marp;
+```
+
+- Preconditions: `@marp-team/marp-core` and `@marp-team/marpit` available
+- Postconditions: Marp instances configured with GROWI options
+- Invariants: Instances are singletons created at module scope
+
+**Implementation Notes**
+- Import `MARP_CONTAINER_CLASS_NAME` from `../consts` instead of defining locally
+- Re-export for backward compatibility (MarpSlides still imports from here)
+
+### Build Script
+
+#### extract-marpit-css.mjs
+
+| Field | Detail |
+|-------|--------|
+| Intent | Generate `marpit-base-css.ts` by running Marp with the same configuration as `growi-marpit.ts` |
+| Requirements | 3.1, 3.2 |
+
+**Responsibilities & Constraints**
+- Replicate the Marp configuration from `growi-marpit.ts` (container classes, inlineSVG, etc.)
+- Generate `slideMarpit.render('')` and `presentationMarpit.render('')` CSS output
+- Write TypeScript file with exported string constants
+- Must run in Node.js ESM without additional tooling (no `tsx`, no `ts-node`)
+
+**Dependencies**
+- External: `@marp-team/marp-core` — Marp rendering engine (P0)
+- External: `@marp-team/marpit` — Element class for container config (P0)
+
+**Implementation Notes**
+- Integration: Added as `"pre:build:src"` script in `package.json`; runs before `vite build`
+- Validation: Script exits with error if CSS extraction produces empty output
+- Risks: Marp options must stay synchronized with `growi-marpit.ts`
+
+## Testing Strategy
+
+### Unit Tests
+- Verify `GrowiSlides` renders correctly with pre-extracted CSS constants (no Marp imports in test)
+- Verify `Slides` renders `GrowiSlides` when `hasMarpFlag` is false/undefined
+- Verify `Slides` renders `MarpSlides` (via Suspense) when `hasMarpFlag` is true
+
+### Build Verification Tests
+- `GrowiSlides.js` build output contains no references to `@marp-team/marp-core` or `@marp-team/marpit`
+- `Slides.js` build output contains dynamic `import()` for MarpSlides
+- `@growi/presentation` builds without errors
+- `@growi/app` builds without errors
+
+### Integration Tests
+- Marp slide page (`marp: true`) renders correctly in inline view
+- Non-Marp slide page (`slide: true`) renders correctly in inline view
+- Presentation modal works for both Marp and non-Marp content
+
+## Performance & Scalability
+
+**Target**: Eliminate ~896KB of Marp-related JavaScript from the async chunk loaded for non-Marp slide rendering.
+
+**Measurement**: Compare the chunk contents before and after optimization using the existing `ChunkModuleStatsPlugin` or manual inspection of build output. The `initial` module count (primary KPI from build-optimization skill) is not directly affected since slides are already in async chunks, but the async chunk size is reduced.

+ 58 - 0
.kiro/specs/optimize-presentation/requirements.md

@@ -0,0 +1,58 @@
+# Requirements Document
+
+## Introduction
+
+The GROWI presentation feature (`@growi/presentation` package) statically imports `@marp-team/marp-core` (~524KB) and `@marp-team/marpit` (~372KB) whenever any slide component loads, even when Marp rendering is not needed. This is because both `MarpSlides` and `GrowiSlides` components statically import `growi-marpit.ts`, which instantiates Marp objects at module scope.
+
+The goal is to decouple heavy Marp dependencies so they are only loaded when a page explicitly uses `marp: true` in its frontmatter, reducing the async chunk size for the common non-Marp slide rendering path and improving overall bundle efficiency.
+
+## Requirements
+
+### Requirement 1: Decouple GrowiSlides from Marp Runtime Dependencies
+
+**Objective:** As a developer, I want GrowiSlides to render without loading `@marp-team/marp-core` or `@marp-team/marpit`, so that non-Marp slide pages do not incur unnecessary module loading overhead.
+
+#### Acceptance Criteria
+1. The `@growi/presentation` build output for GrowiSlides shall not contain import references to `@marp-team/marp-core` or `@marp-team/marpit`.
+2. When a slide page without `marp: true` is rendered, the Presentation module shall render GrowiSlides without loading `@marp-team/marp-core` or `@marp-team/marpit` modules.
+3. The Presentation module shall provide the Marp base CSS (previously generated by `marpit.render('')`) as pre-extracted static string constants, so that GrowiSlides can apply Marp container styling without a runtime Marp dependency.
+4. The `MARP_CONTAINER_CLASS_NAME` constant shall be defined in a shared constants module, not in `growi-marpit.ts`, to avoid transitive Marp imports.
+
+### Requirement 2: Dynamic Loading of MarpSlides
+
+**Objective:** As a developer, I want MarpSlides to be loaded dynamically (on demand), so that the Marp rendering engine is only fetched when a page actually uses Marp.
+
+#### Acceptance Criteria
+1. When a slide page with `marp: true` is rendered, the Presentation module shall dynamically load MarpSlides and render Marp content correctly.
+2. While MarpSlides is loading, the Presentation module shall display a loading indicator (Suspense fallback).
+3. When a slide page without `marp: true` is rendered, the Presentation module shall not trigger the dynamic import of MarpSlides.
+
+### Requirement 3: Build-Time CSS Extraction
+
+**Objective:** As a developer, I want the Marp base CSS to be extracted at build time via an automated script, so that the pre-extracted CSS stays synchronized with the installed `@marp-team/marp-core` version.
+
+#### Acceptance Criteria
+1. The `@growi/presentation` package shall include a build-time script that generates Marp base CSS constants by invoking `slideMarpit.render('')` and `presentationMarpit.render('')`.
+2. When `pnpm run build` is executed for `@growi/presentation`, the build pipeline shall regenerate the CSS constants before compiling source files.
+3. The generated CSS constants file shall be committed to the repository so that `dev` mode works without running the extraction script first.
+
+### Requirement 4: Functional Equivalence
+
+**Objective:** As a user, I want the presentation feature to behave identically after optimization, so that existing Marp and non-Marp presentations continue to work without regression.
+
+#### Acceptance Criteria
+1. When a page with `marp: true` frontmatter is viewed inline, the Presentation module shall render Marp slides with correct styling.
+2. When a page with `slide: true` frontmatter (without `marp: true`) is viewed inline, the Presentation module shall render GrowiSlides with correct styling.
+3. When the presentation modal is opened for a Marp page, the Presentation module shall render Marp slides in the modal with correct fullscreen behavior.
+4. When the presentation modal is opened for a non-Marp slide page, the Presentation module shall render GrowiSlides in the modal with correct fullscreen behavior.
+5. When a non-slide page is viewed, the Presentation module shall not load any slide rendering components (existing lazy-loading behavior preserved).
+
+### Requirement 5: Build Verification
+
+**Objective:** As a developer, I want to verify that the optimization achieves its goal, so that Marp module separation can be confirmed in CI and during development.
+
+#### Acceptance Criteria
+1. The `@growi/presentation` package shall build successfully with `pnpm run build`.
+2. The `@growi/app` package shall build successfully with `turbo run build --filter @growi/app`.
+3. The built `GrowiSlides.js` output shall contain no references to `@marp-team/marp-core` or `@marp-team/marpit`.
+4. The built `Slides.js` output shall contain a dynamic `import()` expression for `MarpSlides`.

+ 84 - 0
.kiro/specs/optimize-presentation/research.md

@@ -0,0 +1,84 @@
+# Research & Design Decisions
+
+## Summary
+- **Feature**: `optimize-presentation`
+- **Discovery Scope**: Extension
+- **Key Findings**:
+  - `GrowiSlides` imports `growi-marpit.ts` only for CSS extraction (`marpit.render('')`) and a string constant — no Marp rendering is performed
+  - `growi-marpit.ts` instantiates `new Marp(...)` at module scope, pulling in `@marp-team/marp-core` (~524KB) and `@marp-team/marpit` (~372KB) unconditionally
+  - All consumers of `Slides.tsx` are already behind SSR-disabled dynamic boundaries (`next/dynamic`, `useLazyLoader`), making `React.lazy` safe to use within the package
+
+## Research Log
+
+### GrowiSlides dependency on growi-marpit.ts
+- **Context**: Investigating why GrowiSlides requires Marp at all
+- **Sources Consulted**: `packages/presentation/src/client/components/GrowiSlides.tsx`, `packages/presentation/src/client/services/growi-marpit.ts`
+- **Findings**:
+  - `GrowiSlides` imports `MARP_CONTAINER_CLASS_NAME` (string `'marpit'`), `presentationMarpit`, `slideMarpit`
+  - Usage: `const { css } = marpit.render('');` — renders empty string to extract base theme CSS
+  - The CSS output is deterministic (same Marp config → same CSS every time)
+  - `MARP_CONTAINER_CLASS_NAME` is a plain string constant co-located with heavy Marp code
+- **Implications**: GrowiSlides can be fully decoupled by pre-extracting the CSS and moving the constant
+
+### Vite build with preserveModules and React.lazy
+- **Context**: Verifying that React.lazy dynamic import works correctly in the package's Vite build output
+- **Sources Consulted**: `packages/presentation/vite.config.ts`
+- **Findings**:
+  - Build uses `preserveModules: true` with `preserveModulesRoot: 'src'` — each source file → separate output module
+  - `nodeExternals({ devDeps: true })` externalizes `@marp-team/marp-core` and `@marp-team/marpit`
+  - Dynamic `import('./MarpSlides')` in source will produce dynamic `import('./MarpSlides.js')` in output
+  - Next.js (app bundler) handles code-splitting of the externalized Marp packages into async chunks
+- **Implications**: React.lazy in Slides.tsx will produce the correct dynamic import boundary in the build output
+
+### SSR safety of React.lazy
+- **Context**: React.lazy does not support SSR in React 18; need to verify all render paths are client-only
+- **Sources Consulted**: Consumer components in apps/app
+- **Findings**:
+  - `PagePresentationModal`: `useLazyLoader` → client-only
+  - `Presentation` wrapper: `next/dynamic({ ssr: false })`
+  - `SlideRenderer`: `next/dynamic({ ssr: false })` in PageView.tsx
+  - No SSR path exists for `Slides.tsx`
+- **Implications**: React.lazy is safe; no need for `next/dynamic` in the shared package
+
+### CSS extraction approach
+- **Context**: Evaluating how to pre-extract the Marp base CSS
+- **Sources Consulted**: `growi-marpit.ts`, package.json scripts, existing `build:vendor-styles` pattern
+- **Findings**:
+  - `slideMarpit.render('')` and `presentationMarpit.render('')` produce deterministic CSS strings
+  - CSS only changes when `@marp-team/marp-core` is upgraded or Marp options change
+  - The package already has a `build:vendor-styles` script pattern for pre-building CSS assets
+  - `tsx` is not available in the workspace; extraction script must use plain Node.js (.mjs)
+- **Implications**: An .mjs extraction script with dynamic imports from the built package or direct Marp usage is the simplest approach
+
+## Design Decisions
+
+### Decision: Pre-extracted CSS constants vs runtime generation
+- **Context**: GrowiSlides needs Marp theme CSS but should not load Marp runtime
+- **Alternatives Considered**:
+  1. Dynamic import of growi-marpit in GrowiSlides — adds async complexity for a static value
+  2. Pre-extract CSS at build time as string constants — zero runtime cost
+  3. CSS file extracted to dist — requires additional CSS import handling
+- **Selected Approach**: Pre-extract CSS as TypeScript string constants in `consts/marpit-base-css.ts`
+- **Rationale**: The CSS is deterministic; generating it at build time eliminates runtime overhead entirely. TypeScript constants integrate seamlessly with existing import patterns.
+- **Trade-offs**: Requires regeneration on Marp version upgrade (mitigated by `pre:build:src` script)
+- **Follow-up**: Verify CSS output matches runtime generation; commit generated file for dev mode
+
+### Decision: React.lazy vs next/dynamic for MarpSlides
+- **Context**: Need dynamic import boundary for MarpSlides within the shared `@growi/presentation` package
+- **Alternatives Considered**:
+  1. `next/dynamic` — Next.js-specific, couples package to framework
+  2. `React.lazy + Suspense` — standard React, works with any bundler
+- **Selected Approach**: `React.lazy + Suspense`
+- **Rationale**: Although the package already uses `next/head`, `React.lazy` is the standard React pattern for code-splitting and avoids further Next.js coupling. All consumer paths already disable SSR, so React.lazy's SSR limitation is irrelevant.
+- **Trade-offs**: Requires `<Suspense>` wrapper; fallback UI is visible during chunk load
+- **Follow-up**: Verify chunk splitting in Next.js production build
+
+### Decision: Shared constants module for MARP_CONTAINER_CLASS_NAME
+- **Context**: `MARP_CONTAINER_CLASS_NAME` is defined in `growi-marpit.ts` but needed by GrowiSlides without Marp dependency
+- **Selected Approach**: Move to `consts/index.ts` (existing shared constants module)
+- **Rationale**: The constant has no dependency on Marp; co-locating it with Marp code creates an unnecessary transitive import
+
+## Risks & Mitigations
+- **CSS drift on Marp upgrade**: Pre-extracted CSS may become stale → `pre:build:src` script auto-regenerates before every build
+- **Suspense flash on Marp load**: Brief loading indicator when MarpSlides loads → Masked by parent `next/dynamic` loading spinner in most paths
+- **Build script compatibility**: `.mjs` script must work in CI and local dev → Use standard Node.js ESM with no external tooling dependencies

+ 22 - 0
.kiro/specs/optimize-presentation/spec.json

@@ -0,0 +1,22 @@
+{
+  "feature_name": "optimize-presentation",
+  "created_at": "2026-03-05T12:00:00Z",
+  "updated_at": "2026-03-05T13:30:00Z",
+  "language": "en",
+  "phase": "tasks-generated",
+  "approvals": {
+    "requirements": {
+      "generated": true,
+      "approved": true
+    },
+    "design": {
+      "generated": true,
+      "approved": true
+    },
+    "tasks": {
+      "generated": true,
+      "approved": false
+    }
+  },
+  "ready_for_implementation": false
+}

+ 49 - 0
.kiro/specs/optimize-presentation/tasks.md

@@ -0,0 +1,49 @@
+# Implementation Plan
+
+- [ ] 1. Set up shared constants and build-time CSS extraction infrastructure
+- [ ] 1.1 Move the Marp container class name constant to the shared constants module and update growi-marpit to import from there
+  - Add the `MARP_CONTAINER_CLASS_NAME` string constant to the existing shared constants module in the presentation package
+  - Update growi-marpit to import the constant from the shared module instead of defining it locally
+  - Re-export the constant from growi-marpit for backward compatibility with MarpSlides
+  - _Requirements: 1.4_
+
+- [ ] 1.2 Create the build-time CSS extraction script
+  - Write a Node.js ESM script that instantiates Marp with the same configuration as growi-marpit (container classes, inlineSVG, emoji/html/math disabled)
+  - The script renders empty strings through both slide and presentation Marp instances to extract their CSS output
+  - Write the CSS strings as exported TypeScript constants to the constants directory
+  - Include a file header comment indicating the file is auto-generated and how to regenerate it
+  - Validate that extracted CSS is non-empty before writing
+  - _Requirements: 3.1_
+
+- [ ] 1.3 Wire the extraction script into the build pipeline and generate the initial CSS file
+  - Add a `pre:build:src` script entry in the presentation package's package.json that runs the extraction script before the main Vite build
+  - Execute the script once to generate the initial pre-extracted CSS constants file
+  - Commit the generated file so that dev mode works without running extraction first
+  - _Requirements: 3.2, 3.3_
+
+- [ ] 2. (P) Decouple GrowiSlides from Marp runtime dependencies
+  - Replace the growi-marpit import in GrowiSlides with imports from the shared constants module and the pre-extracted CSS constants
+  - Replace the runtime `marpit.render('')` call with a lookup of the pre-extracted CSS constant based on the presentation mode flag
+  - After this change, GrowiSlides must have no import path leading to `@marp-team/marp-core` or `@marp-team/marpit`
+  - Depends on task 1 (shared constants and CSS file must exist)
+  - _Requirements: 1.1, 1.2, 1.3_
+
+- [ ] 3. (P) Add dynamic import for MarpSlides in the Slides routing component
+  - Replace the static import of MarpSlides with a React.lazy dynamic import that resolves the named export
+  - Wrap the MarpSlides rendering branch in a Suspense boundary with a simple loading fallback
+  - Keep GrowiSlides as a static import (the common, lightweight path)
+  - The dynamic import ensures MarpSlides and its transitive Marp dependencies are only loaded when `hasMarpFlag` is true
+  - Depends on task 1 (shared constants must exist); parallel-safe with task 2 (different file)
+  - _Requirements: 2.1, 2.2, 2.3_
+
+- [ ] 4. Build verification and functional validation
+- [ ] 4.1 Build the presentation package and verify module separation in the output
+  - Run the presentation package build and confirm it succeeds
+  - Inspect the built GrowiSlides output file to confirm it contains no references to `@marp-team/marp-core` or `@marp-team/marpit`
+  - Inspect the built Slides output file to confirm it contains a dynamic `import()` expression for MarpSlides
+  - _Requirements: 5.1, 5.3, 5.4_
+
+- [ ] 4.2 Build the main GROWI application and verify successful compilation
+  - Run the full app build to confirm no regressions from the presentation package changes
+  - Verify that both Marp and non-Marp slide rendering paths are intact by checking the build completes without type errors
+  - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 5.2_