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.jsoncontains 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.tsline 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.jsonarray - Replaces duplicates with numeric index references in
webhooks.json
Optimization opportunities in generation:
- Add minification step (remove whitespace) - easy, ~60% savings
- Strip unused fields (
summary,availability,category,action) - ~10% additional savings - 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 commitb68ac91(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.jsonsystem. Onlyschedule.jsonandworkflow_call.jsonremain as manual files because they're not real webhooks.
Questions to answer:
-
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? -
workflow_call.json- Same questions. Was this captured from an actual workflow run? -
descriptions.json- Are these descriptions synced from docs.github.com or manually maintained? How do we keep them up to date? -
workflow-v1.0.json- What's the process for adding new workflow syntax (new keys, new event types)?
Recommended actions:
-
Add README files - Each JSON file should have documentation explaining what it's for, how to update it, and who maintains it
-
Automate where possible - Could
schedule.jsonbe generated from a real scheduled workflow run'sgithub.event? Could we capture a sample automatically? -
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
Nullfor completion/hover too (show nothing, since we can't know) - Or remove
workflow_call.jsonentirely 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:
cronstruehas a 44KB main entry (without i18n) vs 238KB with i18n. Bundlers should use the smaller one.yamlis necessary - no lighter alternative for full YAML parsingvscode-*packages are minimal and necessary for LSP compatibility
Areas to Investigate
- ✅ Total bundle size - Analyzed above
- ✅ Specific heavy dependencies -
cronstrueandyamlanalyzed - Tree-shaking - Whether unused code is being properly eliminated
- ✅ Load time impact - Lazy-loaded in github-ui via dynamic import()
- ✅ JSON files for event validation - Main culprit (6.2MB webhooks.json)
- ✅ Minifying the workflow schema JSON file - 112K → can be minified
Potential Optimizations
High Impact
-
Drop 31 unused webhook events - Events like
installation,marketplace_purchase,sponsorship,star,team, etc. are inwebhooks.jsonbut 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 -
Strip unused fields - Remove
summary,availability,category,actionfields that are never used by the language service. OnlybodyParametersanddescriptionHtmlare needed. -
Minify JSON files - Currently pretty-printed with whitespace. Minifying saves ~60%.
-
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 -
Add
"sideEffects"to all package.json files - Enable tree-shaking across all packages:expressions/package.json:"sideEffects": falseworkflow-parser/package.json:"sideEffects": falselanguageservice/package.json:"sideEffects": ["./dist/context-providers/events/eventPayloads.js"]
Medium Impact
-
Minify
workflow-v1.0.jsonschema (112K) - Strip whitespace. Note: This file is hand-authored, not generated from webhook data. -
Minify and strip small JSON files -
schedule.json,descriptions.json:- Minify all (remove whitespace)
- Strip values from
schedule.json(only property names are used)
-
Investigate
workflow_call.jsonusage - See bug section above. This file may be incorrect/useless:- For
on: workflow_call,github.eventis inherited from the calling workflow - Current code returns
Nullfor validation (correct) but usesworkflow_call.jsonfor completion (incorrect?) - Options: Remove file entirely, or fix code to return
Nullfor all modes - Saves 7KB + potentially fixes misleading autocomplete
- For
-
Lazy-load event validation data - Refactor
eventPayloads.tsto load JSON on first use instead of at import time.
Low Impact / Further Investigation
-
Tree-shake unused exports - Ensure webpack is eliminating dead code.
-
Evaluate
cronstruesize - Check if it's worth keeping or replacing with lighter alternative. -
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)
- Add list of valid workflow trigger events (whitelist)
- Filter out events not in whitelist during generation
- Strip unused fields (
summary,availability,category,action) - Output minified JSON (
JSON.stringify(data)instead ofJSON.stringify(data, null, 2))
Phase 1b: Minify/optimize small hand-authored JSON files
- Minify
descriptions.json(18 KB → 17 KB) - Strip values & minify
schedule.json(5.7 KB → 1.8 KB) - Strip values & minify
workflow_call.json(7.3 KB → 2.3 KB) - Minify
workflow-v1.0.json(112 KB → ~90 KB)
Phase 2: Add sideEffects to all package.json files
- Add
"sideEffects": falsetoexpressions/package.json - Add
"sideEffects": falsetoworkflow-parser/package.json - Add
"sideEffects": ["./dist/context-providers/events/eventPayloads.js"]tolanguageservice/package.json
Phase 3: (Optional) Refactor for lazy loading
- Move JSON imports inside functions
- 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_requestevent?
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
- Phase 1: Minify + strip unused data (reduce 6.2MB → ~1.2MB)
- Phase 2: Lazy load webhooks.json in
eventPayloads.ts - 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.tsimportsworkflow-v1.0.jsonat top level BUT:- Only exports a function
getWorkflowSchema()with lazy initialization - No top-level function calls or mutations
- Only exports a function
- 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;
}
Recommended sideEffects Configuration
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.