Files
languageservices/BUNDLE_SIZE_INVESTIGATION.md
T

28 KiB

Bundle Size Investigation

Current State

Package sizes on disk (in github-ui node_modules):

  • @actions/languageservice: 7.9M
  • @actions/workflow-parser: 1.5M
  • @actions/expressions: 560K
  • Total: ~10M

Largest files:

File Size % of total
languageservice/dist/context-providers/events/webhooks.json 6.2M 62%
languageservice/dist/context-providers/events/objects.json 948K 9.5%
workflow-parser/dist/workflow-v1.0.json 112K 1%
languageservice/dist/context-providers/descriptions.json 20K <1%

JSON File Analysis

What webhooks.json is used for

Provides autocomplete and validation for github.* context expressions. When you type ${{ github.event. the language service uses this data to:

  • Suggest available properties based on event type (push, pull_request, etc.)
  • Provide descriptions for hover tooltips
  • Validate property access is valid for the event type

Field usage analysis

Field Location Size Used for Autocomplete Used for Validation Used for Hover
bodyParameters[].description Inside each param Part of bodyParams Documentation popup Property existence Descriptions
bodyParameters[].name/type/etc Inside each param 1.55 MB total Property names Property existence Structure
description Top-level on event 17 KB Defined but unused
summary Top-level on event 155 KB
availability Top-level on event 7 KB
category Top-level on event 3 KB
action Top-level on event 2 KB

Key insight: bodyParameters (including nested description fields) is used for ALL features. The top-level fields (summary, description, availability, category, action) are defined in the TypeScript types but never actually accessed in code - they can be stripped.

Why top-level description/summary shouldn't be used for workflow events

Question: Could we use the webhooks.json top-level description or summary fields to enhance autocomplete/hover for the on: field?

Answer: No - they serve different purposes and the existing solution is better.

Comparison:

Source Example for push Purpose
workflow-v1.0.json (current) "Runs your workflow when you push a commit or tag." User-facing - explains what triggers the workflow
webhooks.json description "A push was made to a repository branch..." API-facing - describes the GitHub API event
webhooks.json summary "This event occurs when a commit or tag is pushed. To subscribe to this event, a GitHub App must have at least read-level access..." App developer-facing - API permissions info

The current solution is correct:

  • workflow-v1.0.json contains workflow-specific event descriptions written for GitHub Actions users
  • These are shown in autocomplete/hover when completing on: push, on: pull_request, etc.
  • Located in languageservice/src/value-providers/definition.ts line 46: description: def.description

The webhooks.json descriptions would be wrong:

  • Written for GitHub App developers, not GitHub Actions users
  • Include irrelevant details (API permissions, subscription info)
  • Don't explain what happens in the context of a workflow

Conclusion: Keep the top-level fields stripped - they're not needed and would be confusing if used.

Minification analysis

File Pretty Size Minified Size Savings
webhooks.json 4.1 MB 1.6 MB 2.5 MB (60.5%)
objects.json 666 KB 325 KB 341 KB (51.3%)
workflow-v1.0.json 91 KB 70 KB 22 KB (23.5%)

The files are NOT minified! Just minifying saves 60%.

Compression analysis (gzip)

Production servers typically gzip assets. Here's what matters for network transfer:

File Original Minified Gzipped Min+Gzip
webhooks.json 4.0 MB 1.6 MB 198 KB 90 KB
objects.json 651 KB 317 KB 38 KB 23 KB
workflow-v1.0.json 91 KB 70 KB 13 KB 13 KB

What matters for different concerns:

Concern What matters
Network transfer Compressed size (gzip/brotli) - already small (~126 KB total)
npm package size Uncompressed size on disk - affects install times
Memory usage Parsed JSON object size in memory
Parse time Uncompressed size (must decompress before parsing)

Key insight: Network transfer is NOT the main concern (~126 KB gzipped). Minifying still matters for:

  • Smaller npm package size (better install times)
  • Less to decompress on client
  • Faster JSON parsing (less text to parse)

How the files are generated

The JSON files are auto-generated from GitHub's official REST API description:

npm run update-webhooks

Source: github:github/rest-api-description (GitHub's OpenAPI spec)

Generation script: languageservice/script/webhooks/index.ts

  • Reads webhook definitions from the dereferenced OpenAPI schema
  • Extracts body parameters, descriptions, summaries
  • Runs deduplication to create objects.json (shared parameters stored once, referenced by index)
  • Outputs pretty-printed JSON (not minified)

Current deduplication strategy (deduplicate.ts):

  • Finds body parameters that appear in multiple webhooks
  • Stores them once in objects.json array
  • Replaces duplicates with numeric index references in webhooks.json

Optimization opportunities in generation:

  1. Add minification step (remove whitespace) - easy, ~60% savings
  2. Strip unused fields (summary, availability, category, action) - ~10% additional savings
  3. Consider more aggressive deduplication (e.g., dedupe descriptions, nested objects)

workflow-v1.0.json (workflow schema)

Hand-authored - not generated. Located in workflow-parser/src/.

Optimization: Minify at build time (112K pretty → smaller minified).

Other Small JSON Files

File Purpose Pretty Minified Further Optimized
descriptions.json Hover descriptions for contexts/functions 18 KB 17 KB N/A (all used)
schedule.json Sample github.event for schedule trigger 5.7 KB 5.1 KB 1.8 KB (strip values)
workflow_call.json Sample github.event for reusable workflows 7.3 KB 6.5 KB 2.3 KB (strip values)

Why schedule.json / workflow_call.json exist:

These events are NOT webhooks - they're internal GitHub Actions triggers that don't appear in the REST API webhook definitions. The files provide sample github.event payloads so the language service knows what properties to autocomplete:

User types: ${{ github.event.repository.owner.login }}
                              ↑
Language service walks schedule.json to find valid property names

The code (eventPayloads.ts lines 109-116) uses mergeObject() to recursively extract property names - the actual values are never used.

Key insight for schedule.json / workflow_call.json: These files provide sample event payloads. The code only uses property names (for autocomplete like github.event.repository.owner.login), not values. The actual values (URLs, IDs, emails) can be replaced with null:

// Original (5.1 KB)
{"repository":{"id":186853002,"name":"Hello-World","owner":{"login":"Codertocat",...},...},...}

// Stripped (1.8 KB) - same autocomplete functionality
{"repository":{"id":null,"name":null,"owner":{"login":null,...},...},...}

Savings: ~65% smaller for these files.

JSON File Maintenance & Documentation

TODO: Document maintenance procedures

File Source How to Update Documented?
webhooks.json + objects.json npm run update-webhooks from rest-api-description Run script ⚠️ Partial (in script)
workflow-v1.0.json Hand-authored Manual edits No
descriptions.json Hand-authored Manual edits No
schedule.json Hand-authored sample payload Manual edits No - unclear origin
workflow_call.json Hand-authored sample payload Manual edits No - unclear origin

Historical context (from git history):

  • schedule.json - Added in commit b68ac91 (Dec 2022) by Beth Brennan in "Use payload schema for events"

    • Uses "Codertocat/Hello-World" sample data (appears to be from GitHub's webhook documentation examples)
    • No documentation on where this came from or how to update it
    • Question: Is this based on a real scheduled workflow run? How do we know it includes all possible properties?
  • workflow_call.json - Same commit, similar questions

  • Many other event JSON files were added in that same commit, but were later replaced by the generated webhooks.json system. Only schedule.json and workflow_call.json remain as manual files because they're not real webhooks.

Questions to answer:

  1. schedule.json - Where did this sample payload come from? Is it based on a real event? How do we know it's complete/accurate? Does it need updating when GitHub adds new repository properties?

  2. workflow_call.json - Same questions. Was this captured from an actual workflow run?

  3. descriptions.json - Are these descriptions synced from docs.github.com or manually maintained? How do we keep them up to date?

  4. workflow-v1.0.json - What's the process for adding new workflow syntax (new keys, new event types)?

  1. Add README files - Each JSON file should have documentation explaining what it's for, how to update it, and who maintains it

  2. Automate where possible - Could schedule.json be generated from a real scheduled workflow run's github.event? Could we capture a sample automatically?

  3. Add tests - Validate that sample payloads match expected structure

⚠️ BUG: workflow_call.json may be incorrect/useless

Finding: For on: workflow_call (reusable workflows), the github.event context is inherited from the calling workflow. If the caller was triggered by push, then github.event contains push data. If by pull_request, it contains PR data.

Current behavior in github.ts:

// Line 87-89 - For VALIDATION mode, returns Null (any value allowed)
if (eventsConfig.workflow_call && mode == Mode.Validation) {
  return new data.Null();
}

// But for COMPLETION/HOVER mode, falls through and uses workflow_call.json!

Problem: workflow_call.json contains generic repo/sender/org data, but this is WRONG for autocomplete. When you type ${{ github.event. in a reusable workflow, showing repository, sender, etc. is misleading because:

  • The actual properties depend on how the workflow was called
  • Could be push properties, PR properties, or anything else

Recommendation:

  • Either return Null for completion/hover too (show nothing, since we can't know)
  • Or remove workflow_call.json entirely since it's actively misleading
  • This would save 7KB and fix a bug!

npm Package Sizes

The actual npm package sizes (gzipped tarballs) are much smaller than disk size:

Package Disk Size Package Size (gzipped) Unpacked
@actions/languageservice 7.9M 368 KB 7.7 MB
@actions/workflow-parser 1.5M 98 KB 548 KB
@actions/expressions 560K 34 KB 153 KB
Total ~10M ~500 KB ~8.4 MB

Key insight: npm install downloads ~500KB gzipped. The disk/memory impact is ~8.4 MB unpacked.

Dependencies Analysis

Direct dependencies:

Package Disk Size Used By Notes
yaml 1.4 MB workflow-parser, languageservice Full YAML parser, well-structured
cronstrue 1.4 MB workflow-parser Cron → human text. Main: 44KB (no i18n)
vscode-languageserver-types 396 KB languageservice Type definitions for LSP
vscode-languageserver-textdocument 72 KB languageservice Text document handling
vscode-uri 256 KB languageservice URI parsing

Observations:

  • cronstrue has a 44KB main entry (without i18n) vs 238KB with i18n. Bundlers should use the smaller one.
  • yaml is necessary - no lighter alternative for full YAML parsing
  • vscode-* packages are minimal and necessary for LSP compatibility

Areas to Investigate

  1. Total bundle size - Analyzed above
  2. Specific heavy dependencies - cronstrue and yaml analyzed
  3. Tree-shaking - Whether unused code is being properly eliminated
  4. Load time impact - Lazy-loaded in github-ui via dynamic import()
  5. JSON files for event validation - Main culprit (6.2MB webhooks.json)
  6. Minifying the workflow schema JSON file - 112K → can be minified

Potential Optimizations

High Impact

  1. Drop 31 unused webhook events - Events like installation, marketplace_purchase, sponsorship, star, team, etc. are in webhooks.json but cannot be used as workflow triggers. Confirmed against GitHub's official docs.

    Metric Before After Savings
    Events 63 32 31 dropped
    Size 1.76 MB 1.42 MB 19%

    Events to drop:

    code_scanning_alert, commit_comment, dependabot_alert, deploy_key,
    github_app_authorization, installation, installation_repositories,
    installation_target, marketplace_purchase, member, membership, meta,
    org_block, organization, package, ping, projects_v2, projects_v2_item,
    pull_request_review_thread, repository, repository_import,
    repository_vulnerability_alert, secret_scanning_alert,
    secret_scanning_alert_location, security_advisory, security_and_analysis,
    sponsorship, star, team, team_add, workflow_job
    
  2. Strip unused fields - Remove summary, availability, category, action fields that are never used by the language service. Only bodyParameters and descriptionHtml are needed.

  3. Minify JSON files - Currently pretty-printed with whitespace. Minifying saves ~60%.

  4. Combined impact estimate:

    Optimization webhooks.json objects.json
    Original 6.2 MB 948 KB
    Drop unused events 5.0 MB (-19%) 770 KB (-19%)
    Strip unused fields 3.0 MB (-40%) 460 KB (-40%)
    Minify 1.2 MB (-60%) 225 KB (-52%)
    Gzipped (network) ~60 KB ~20 KB
  5. Add "sideEffects" to all package.json files - Enable tree-shaking across all packages:

    • expressions/package.json: "sideEffects": false
    • workflow-parser/package.json: "sideEffects": false
    • languageservice/package.json: "sideEffects": ["./dist/context-providers/events/eventPayloads.js"]

Medium Impact

  1. Minify workflow-v1.0.json schema (112K) - Strip whitespace. Note: This file is hand-authored, not generated from webhook data.

  2. Minify and strip small JSON files - schedule.json, descriptions.json:

    • Minify all (remove whitespace)
    • Strip values from schedule.json (only property names are used)
  3. Investigate workflow_call.json usage - See bug section above. This file may be incorrect/useless:

    • For on: workflow_call, github.event is inherited from the calling workflow
    • Current code returns Null for validation (correct) but uses workflow_call.json for completion (incorrect?)
    • Options: Remove file entirely, or fix code to return Null for all modes
    • Saves 7KB + potentially fixes misleading autocomplete
  4. Lazy-load event validation data - Refactor eventPayloads.ts to load JSON on first use instead of at import time.

Low Impact / Further Investigation

  1. Tree-shake unused exports - Ensure webpack is eliminating dead code.

  2. Evaluate cronstrue size - Check if it's worth keeping or replacing with lighter alternative.

  3. Bundle analysis - Run webpack-bundle-analyzer to see actual bundled sizes after minification/compression.

Implementation Plan

Phase 1: Update generation script (languageservice/script/webhooks/index.ts)

  1. Add list of valid workflow trigger events (whitelist)
  2. Filter out events not in whitelist during generation
  3. Strip unused fields (summary, availability, category, action)
  4. Output minified JSON (JSON.stringify(data) instead of JSON.stringify(data, null, 2))

Phase 1b: Minify/optimize small hand-authored JSON files

  1. Minify descriptions.json (18 KB → 17 KB)
  2. Strip values & minify schedule.json (5.7 KB → 1.8 KB)
  3. Strip values & minify workflow_call.json (7.3 KB → 2.3 KB)
  4. Minify workflow-v1.0.json (112 KB → ~90 KB)

Phase 2: Add sideEffects to all package.json files

  1. Add "sideEffects": false to expressions/package.json
  2. Add "sideEffects": false to workflow-parser/package.json
  3. Add "sideEffects": ["./dist/context-providers/events/eventPayloads.js"] to languageservice/package.json

Phase 3: (Optional) Refactor for lazy loading

  1. Move JSON imports inside functions
  2. Remove top-level hydration code, make it lazy

Phase 4: Automated JSON updates via GitHub Actions

Create workflows to automatically keep JSON files up to date:

4a: Webhook JSON auto-update workflow

# .github/workflows/update-webhooks.yml
name: Update webhook definitions
on:
  schedule:
    - cron: '0 0 * * 1'  # Weekly on Monday
  workflow_dispatch:      # Manual trigger

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npm run update-webhooks
      - name: Create PR if changes
        uses: peter-evans/create-pull-request@v5
        with:
          title: "chore: Update webhook definitions"
          body: |
            Automated update from `rest-api-description` package.
            
            This PR was created automatically by the update-webhooks workflow.
          branch: auto/update-webhooks
          delete-branch: true  # Delete old branch, creates fresh PR each time
          commit-message: "chore: Update webhook definitions"

4b: Schedule/workflow_call JSON auto-update workflow

Create a workflow that runs an actual scheduled workflow and captures github.event:

# .github/workflows/capture-schedule-payload.yml
name: Capture schedule event payload
on:
  schedule:
    - cron: '0 0 1 * *'  # Monthly on the 1st
  workflow_dispatch:

jobs:
  capture:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Capture github.event
        run: |
          echo '${{ toJSON(github.event) }}' > /tmp/schedule-event.json
          # Strip to just property structure (values → null)
          node -e "
            const fs = require('fs');
            const strip = (o) => {
              if (Array.isArray(o)) return o.length ? [strip(o[0])] : [];
              if (o && typeof o === 'object') return Object.fromEntries(
                Object.entries(o).map(([k,v]) => [k, strip(v)])
              );
              return null;
            };
            const data = JSON.parse(fs.readFileSync('/tmp/schedule-event.json'));
            const stripped = strip(data);
            fs.writeFileSync(
              'languageservice/src/context-providers/events/schedule.json',
              JSON.stringify(stripped, null, 2)
            );
          "
      
      - name: Create PR if changes
        uses: peter-evans/create-pull-request@v5
        with:
          title: "chore: Update schedule.json payload structure"
          body: |
            Captured fresh `github.event` structure from a real scheduled workflow run.
            
            This ensures autocomplete suggestions match the actual event payload.
          branch: auto/update-schedule-json
          delete-branch: true
          commit-message: "chore: Update schedule.json from live event"

4c: Workflow_call payload capture

Similar approach - create a reusable workflow that calls itself and captures github.event:

# .github/workflows/capture-workflow-call-payload.yml  
name: Capture workflow_call event payload
on:
  workflow_call:
  workflow_dispatch:

jobs:
  capture:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Capture and update workflow_call.json
        if: github.event_name == 'workflow_call'
        run: |
          # Similar to schedule capture above
          echo '${{ toJSON(github.event) }}' | node -e "..." > workflow_call.json
      - name: Trigger self as reusable workflow
        if: github.event_name == 'workflow_dispatch'
        uses: ./.github/workflows/capture-workflow-call-payload.yml

Benefits:

  • JSON files stay up to date automatically
  • PRs are created for review (not auto-merged)
  • Captures real event structures, not guessed samples
  • Weekly/monthly schedule catches GitHub API changes

Validation Stages Analysis

The current validate() function does everything in one pass. We could split it into stages that load progressively:

Current Loading Cascade

validate() called
  └─ imports workflow-parser
      └─ imports workflow-v1.0.json (112KB) ← loaded immediately
  └─ parseWorkflow() → YAML parse + schema validation
  └─ additionalValidations()
      └─ getContext() → imports github.ts
          └─ imports eventPayloads.ts
              └─ imports webhooks.json (6.2MB) ← loaded immediately

Potential Validation Stages

Stage What it validates Data needed Size
1. YAML Syntax Valid YAML? Quotes closed? Indentation? YAML parser (bundled) ~0
2. Workflow Schema Valid jobs:, steps:, runs-on:? workflow-v1.0.json 112KB
3. Expression Syntax Valid ${{ }} syntax? Functions exist? Expression parser ~0
4. Context Validation github.sha, env.FOO exist? Just code ~0
5. Event Payload Validation github.event.pull_request.title exists? webhooks.json 6.2MB

Key Insight

Stages 1-4 can run with minimal data (~112KB). Only Stage 5 needs the 6.2MB webhook data.

Expression syntax (${{ secrets.FOO }}) is different from event payload validation (${{ github.event.issue.number }}):

  • Expression syntax: Is this a valid expression? Does the function exist?
  • Event payload: Does this specific property exist on the pull_request event?

Options for Progressive Loading

Option A: Lazy load webhooks.json (simplest)

// eventPayloads.ts - defer import until first use
let webhooksData: Webhooks | null = null;
async function getWebhooks() {
  if (!webhooksData) {
    const { default: data } = await import("./webhooks.json");
    webhooksData = data;
  }
  return webhooksData;
}
  • Pro: Minimal code changes
  • Con: Still blocks when github.event.* is first accessed

Option B: Multi-pass validation in languageservice

// New exports from @actions/languageservice
export { validateSchema } from "./validate-schema";           // Fast
export { validateExpressions } from "./validate-expressions"; // Needs webhooks
export { validate } from "./validate";                        // Combined (current)
  • Pro: Clean API, consumer controls loading
  • Con: More work, API change

Option C: Multi-pass validation in github-ui

// github-ui can show partial results
const schemaErrors = await validate(doc);  // Returns what it can immediately
// Later, more errors may arrive as webhooks.json loads
  • Pro: No languageservice changes
  • Con: Complex state management in consumer

Recommendation

  1. Phase 1: Minify + strip unused data (reduce 6.2MB → ~1.2MB)
  2. Phase 2: Lazy load webhooks.json in eventPayloads.ts
  3. Phase 3 (future): Consider multi-pass API if needed

The lazy loading approach gives 90% of the benefit with 10% of the complexity.

Side Effects Analysis

Need to verify the packages have no side effects before adding "sideEffects": false:

  • @actions/languageservice - Has ONE file with side effects
  • @actions/workflow-parser - No side effects
  • @actions/expressions - No side effects

Common side effects to look for:

  • Top-level function calls (not just definitions)
  • Modifying global objects (Object.prototype, window, etc.)
  • Polyfills
  • CSS imports (not applicable here)

JSON Files Imported at Top Level

Package File JSON Imported Size Has Side Effects?
languageservice eventPayloads.ts webhooks.json 6.2 MB ⚠️ YES (mutation)
languageservice eventPayloads.ts objects.json 948 KB ⚠️ YES (mutation)
languageservice eventPayloads.ts schedule.json 6 KB ⚠️ YES (mutation)
languageservice eventPayloads.ts workflow_call.json 8 KB ⚠️ YES (mutation)
languageservice descriptions.ts descriptions.json 20 KB No
workflow-parser workflow-schema.ts workflow-v1.0.json 112 KB No
expressions (none) (none) - No

Findings

@actions/expressions - No side effects

  • No JSON imports
  • No top-level code execution
  • Can use "sideEffects": false

@actions/workflow-parser - No side effects

  • workflow-schema.ts imports workflow-v1.0.json at top level BUT:
    • Only exports a function getWorkflowSchema() with lazy initialization
    • No top-level function calls or mutations
  • Can use "sideEffects": false

@actions/languageservice - ⚠️ HAS ONE FILE with side effects

descriptions.ts - No side effects

  • Imports descriptions.json (20KB) at top level
  • Only exports functions, no top-level execution

eventPayloads.ts - ⚠️ HAS SIDE EFFECTS

// Lines 3-7: JSON imports at top level (7.2MB total)
import webhookObjects from "./objects.json";
import webhooks from "./webhooks.json";
import schedule from "./schedule.json";
import workflow_call from "./workflow_call.json";

// Lines 85-93: Executes at module load time, mutates data
getWebhookPayload("workflow_dispatch", "default");
const inputs = webhookPayloads?.["workflow_dispatch"]?.["default"].bodyParameters.find(p => p.name === "inputs");
if (inputs) {
  delete inputs.childParamsGroups;
}

expressions/package.json:

"sideEffects": false

workflow-parser/package.json:

"sideEffects": false

languageservice/package.json:

"sideEffects": ["./dist/context-providers/events/eventPayloads.js"]

Impact: Allows webpack to tree-shake unused exports. Without this, webpack assumes all imports may have side effects and keeps everything.

Optional: Refactor eventPayloads.ts to Remove Side Effects

To allow "sideEffects": false for the entire languageservice package, refactor the mutation code:

// Before: Top-level mutation
getWebhookPayload("workflow_dispatch", "default");
const inputs = webhookPayloads?.["workflow_dispatch"]?.["default"].bodyParameters.find(p => p.name === "inputs");
if (inputs) {
  delete inputs.childParamsGroups;
}

// After: Lazy initialization inside function
let initialized = false;
function ensureInitialized() {
  if (initialized) return;
  initialized = true;
  // ... mutation code here
}

export function getEventPayload(...) {
  ensureInitialized();
  // ... rest of function
}

This would allow full tree-shaking AND defer the 7.2MB JSON load until first use.