MCPInstall

plugin-bundle-size

Optimise Grafana app plugin bundle size using React.lazy, Suspense, and webpack code splitting. Use when the user asks to reduce plugin bundle size, optimise module.js, add code splitting, improve initial plugin load performance, split plugin chunks, lazy load plugin pages, or help implement lazy loading in a Grafana app plugin. Triggers on phrases like "optimise plugin bundle size", "module.js is too large", "plugin is slow to load", "code split the plugin", "reduce initial JS payload", or "help me with Suspense in my plugin".

Installs
4.2K
Last 24h
+27
Trending rank
#4658
Security
Pass

Install

Works with every agent the skills CLI supports.

npx skills add https://github.com/grafana/skills --skill plugin-bundle-size
  1. 01

    Install

    Run the command in your project. The CLI asks which agents to install for.

  2. 02

    Describe the task

    Ask your agent for something this skill covers. It loads plugin-bundle-size automatically when relevant.

  3. 03

    Review the result

    The agent follows the SKILL.md steps, scripts and references bundled with the skill.

Security audits

Independent scans of the skill's instructions and bundled files.

Gen Agent Trust Hub PassSAFE ·The skill provides a comprehensive guide for optimizing Grafana plugin bundle sizes using standard React features and official Grafana development tools. It includes instructions for setting up bundle size reporting via GitHub Actions and uses reputable analysis tools. No malicious patterns or security risks were detected.
Socket PassNo alerts
Snyk PassLOW ·Risk: LOW · No issues
ZeroLeaks PassNONE ·Score: 93/100 · 2 sections analyzed

SKILL.md

The instructions your agent reads when the skill is triggered.

Grafana plugin bundle size optimisation

module.js is the render-blocking entry point for every Grafana app plugin. The smaller it is, the less impact the plugin has on Grafana's overall startup time. A well-split plugin should have a module.js under ~200 KB that contains nothing but lazy-loaded wrappers — all feature code loads on demand.

Target: ~15–25 JS chunks total. Fewer means too little splitting; far more (50+) means over-engineering.

Risk levels

Not all splitting opportunities carry the same risk. Apply them in this order:

LevelWhatRiskImpact
Safemodule.tsx lazy wrappers (Priority 1)Very low — no behaviour changeHighest — module.js drops 90%+
SafeRoute-level lazy() (Priority 2)Low — each route is self-containedHigh — one chunk per route
SafeExtension lazy() (Priority 3)Low — extensions are isolatedMedium — independent chunk per extension
ModerateComponent registries / tab panels (Priority 4)Medium — verify Suspense placementMedium — splits heavy pages further
Do not touchVendor libraries (@grafana/scenes, @reduxjs/toolkit)N/AN/A — webpack splits these automatically
Do not touchShared utility components (Markdown, Spinner) used across many filesHigh churn, many callsitesLow — already in shared vendor chunks

When in doubt, stop after Priority 2. Routes alone typically reduce module.js by 95%+.


Step 1: Add bundle size CI reporting (recommended)

Add the grafana/plugin-actions/bundle-size action to get automatic bundle size comparison comments on every PR. This posts a table showing entry point size changes, file count diffs, and total bundle impact.

Root-level plugins (plugin at repo root):

# .github/workflows/bundle-size.yml
name: Bundle Size
on:
  pull_request:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  bundle-size:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write
      pull-requests: write
      actions: read
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: .nvmrc
      - name: Install and build
        run: yarn install
      - name: Bundle Size
        uses: grafana/plugin-actions/bundle-size@a66a1c96cdbb176f9cccf10cf23593e250db7cce # bundle-size/v1.1.0

Subdirectory plugins (e.g. plugin/ in a monorepo):

The action's install step runs at the repo root and cannot find yarn.lock in a subdirectory. Work around this by installing deps yourself and symlinking to root:

jobs:
  bundle-size:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write
      pull-requests: write
      actions: read
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: ./plugin/.nvmrc
      - name: Install dependencies
        working-directory: ./plugin
        run: yarn install
      - name: Symlink plugin to root for bundle-size action
        run: |
          ln -s plugin/yarn.lock yarn.lock
          ln -s plugin/package.json package.json
          ln -s plugin/.yarnrc.yml .yarnrc.yml
          ln -s plugin/node_modules node_modules
      - name: Bundle Size
        uses: grafana/plugin-actions/bundle-size@a66a1c96cdbb176f9cccf10cf23593e250db7cce # bundle-size/v1.1.0
        with:
          working-directory: ./plugin

How it works: On push to main, builds and uploads a baseline artifact. On PRs, compares against it and posts a diff comment. Use workflow_dispatch to generate the first baseline.

Reference: grafana-k8s-plugin workflow


Step 2: Detect plugin context

# Confirm this is an app plugin (type: "app" — datasource/panel plugins have different needs)
jq -r '"\(.id) — \(.type)"' src/plugin.json

# Locate the entry point
ls src/module.ts src/module.tsx 2>/dev/null

# Measure the current PRODUCTION bundle size BEFORE making any changes
# Dev builds are unminified and much larger — always measure production
yarn build 2>/dev/null || npm run build
echo "=== module.js ===" && ls -lah dist/module.js
echo "=== all JS chunks ===" && ls -lah dist/*.js | sort -k5 -rh | head -20
echo "=== chunk count ===" && ls dist/*.js | wc -l

Record the baseline. A pre-split plugin commonly has a module.js of 1–3 MB with no other JS chunks.


Step 3: Check and update create-plugin

The @grafana/create-plugin tool controls .config/webpack/, .config/jest/, and other build scaffolding. Updating it often unlocks faster SWC compilation and better chunk output.

cat .config/.cprc.json 2>/dev/null || grep '"@grafana/create-plugin"' package.json
npm view @grafana/create-plugin version
npx @grafana/create-plugin@latest update

After updating, review the diff (especially .config/webpack/webpack.config.ts) and run a test build. If the plugin has a top-level webpack.config.ts that webpack-merges the base config, review the merge for conflicts.


Step 4: Analyse the codebase — find what to split

Do not start implementing until you have read all of these.

# Entry point — look for direct (non-lazy) imports of App, ConfigPage, exposeComponent targets
cat src/module.ts 2>/dev/null || cat src/module.tsx

# Root App component — look for direct page/route imports that should be lazy
cat src/App.tsx src/components/App.tsx src/feature/app/components/App.tsx 2>/dev/null | head -80

# Extension registrations — each should become an independent chunk
grep -r "exposeComponent\|addComponent\|addLink" src/ --include="*.ts" --include="*.tsx" -n

# Exported side-effect singletons (Faro, analytics) — must be extracted before splitting
grep -n "^export const\|^export let" src/module.ts src/module.tsx 2>/dev/null
grep -rn "from '.*module'" src/ --include="*.ts" --include="*.tsx" | grep -v node_modules

# Heavy synchronous imports
grep -rn "from 'monaco-editor\|@codemirror\|d3\b\|recharts\|chart\.js" \
  src/ --include="*.ts" --include="*.tsx" | grep -v node_modules

Key rule: If a file is imported by module.ts directly (even transitively), it ends up in module.js. Everything reachable from a lazy boundary becomes its own chunk.


Step 5: Implement splits — in priority order

Named vs default exports: React.lazy() requires a default export. Most Grafana plugin components use named exports — use .then() to re-map:

// Named export
const LazyMyComp = lazy(() => import('./MyComponent').then(m => ({ default: m.MyComponent })));
// Default export
const LazyMyComp = lazy(() => import('./MyComponent'));

Priority 1: module.tsx (highest impact, always do this first)

If the entry point is module.ts, rename it: git mv src/module.ts src/module.tsx

Make module.tsx import nothing from feature code except through lazy():

import React, { lazy, Suspense } from 'react';
import { AppPlugin, AppRootProps } from '@grafana/data';
import { LoadingPlaceholder } from '@grafana/ui';

import type { MyExtensionProps } from './extensions/MyExtension';  // import type — erased at compile time
import type { JsonData } from './features/app/state/slice';

// Lazy Faro init — keeps @grafana/faro-react out of module.js
let faroInitialized = false;
async function initFaro() {
  if (faroInitialized) { return; }
  faroInitialized = true;
  const { initializeFaro } = await import('faro');
  initializeFaro();
}

const LazyApp = lazy(async () => {
  await initFaro();
  return import('./features/app/App').then(m => ({ default: m.App }));
});

function App(props: AppRootProps<JsonData>) {
  return <Suspense fallback={<LoadingPlaceholder text="" />}><LazyApp {...props} /></Suspense>;
}

const LazyMyExtension = lazy(() =>
  import('./extensions/MyExtension').then(m => ({ default: m.MyExtension }))
);
function MyExtension(props: MyExtensionProps) {
  return <Suspense fallback={<LoadingPlaceholder text="" />}><LazyMyExtension {...props} /></Suspense>;
}

export const plugin = new AppPlugin<JsonData>().setRootPage(App);
plugin.exposeComponent({ id: 'my-plugin/my-extension/v1', title: 'My Extension', component: MyExtension });

Key details:

  • import type for props prevents webpack from following the import into the eager bundle
  • Use new AppPlugin<JsonData>() if App uses AppRootProps<JsonData> — without the generic, setRootPage() type won't match
  • Remove any App as unknown as ComponentClass<AppRootProps> cast — the lazy wrapper is a valid function component

Expected impact: module.js drops from MB range to ~50–200 KB.

Singletons (e.g. Faro): If module.ts has export const faro = initializeFaro(), do NOT keep it as a top-level import. Extract it to src/faro.ts, update all internal imports from '*/module' → '*/faro', then use the dynamic initFaro() pattern above.


Priority 2: Route-based splitting in App.tsx

import React, { lazy, Suspense } from 'react';
import { Route, Routes } from 'react-router-dom';
import { LoadingPlaceholder } from '@grafana/ui';

const HomePage     = lazy(() => import('../pages/Home'));
const SettingsPage = lazy(() => import('../pages/Settings'));
const DetailPage   = lazy(() => import('../pages/Detail'));

function App(props: AppRootProps) {
  return (
    <Suspense fallback={<LoadingPlaceholder text="" />}>
      <Routes>
        <Route path="home"       element={<HomePage />} />
        <Route path="settings"   element={<SettingsPage />} />
        <Route path="detail/:id" element={<DetailPage />} />
        <Route path=""           element={<HomePage />} />
      </Routes>
    </Suspense>
  );
}
export default App;

Bypass barrel files: Target the actual component file in the import(), not an index.ts barrel that re-exports multiple things:

// Risky — barrel may pull in other heavy modules
const Catalog = lazy(() => import('features/catalog'));
// Better — only pulls in Catalog's tree
const Catalog = lazy(() => import('features/catalog/Catalog').then(m => ({ default: m.Catalog })));

Priority 3: Extension components

Each extension should export default its component. Use fallback={null} for extensions that load quickly:

// src/extensions/MyExtension.tsx
export default function MyExtension(props: MyExtensionProps) {
  return <AppProviders><MyExtensionContent {...props} /></AppProviders>;
}

Surgical split: If an extension wrapper must stay eager in module.tsx, lazy-load the heavy component it renders:

const HeavyInner = lazy(() => import('components/features/HeavyInner'));
export function MyExtension() {
  return <Suspense fallback={<LoadingPlaceholder text="" />}><HeavyInner /></Suspense>;
}

Priority 4: Component registries and tab panels

For arrays of objects containing React components (e.g. tab panels), lazy-load each entry. Critical: ensure a <Suspense> boundary exists where the component renders.

const ConfigDetails = lazy(() => import('./ConfigDetails/ConfigDetails').then(m => ({ default: m.ConfigDetails })));
const Overview      = lazy(() => import('./Overview/Overview').then(m => ({ default: m.Overview })));

const tabs = [
  { id: 'overview', component: Overview },
  { id: 'config',   component: ConfigDetails },
];

// In the parent that renders the active tab:
<Suspense fallback={<LoadingPlaceholder text="" />}>
  {ActiveTab && <ActiveTab />}
</Suspense>

For datasource plugins (setConfigEditor, setQueryEditor, VariableSupport, AnnotationSupport), see references/datasource-plugins.md.


Step 6: Group related chunks if over-splitting

If the build produces more than ~25 JS files, use webpack magic comments:

const FleetList   = lazy(() => import(/* webpackChunkName: "fleet" */ '../pages/FleetList'));
const FleetDetail = lazy(() => import(/* webpackChunkName: "fleet" */ '../pages/FleetDetail'));

One webpackChunkName per logical feature area. Don't group unrelated pages.


Step 7: Measure and verify

yarn build 2>/dev/null || npm run build
echo "=== module.js ===" && ls -lah dist/module.js
echo "=== all JS chunks (largest first) ===" && ls -lah dist/*.js | sort -k5 -rh | head -30
echo "=== chunk count ===" && ls dist/*.js | wc -l
MetricTarget
module.js size< 200 KB
Total JS chunk count15–25
Largest single chunk< 1 MB
# Analyse bundle composition if a chunk is unexpectedly large
npx webpack-bundle-analyzer dist/stats.json 2>/dev/null

Step 8: Test the running plugin

  1. Open the plugin in a Grafana instance
  2. Navigate to every route — each triggers a new chunk download
  3. DevTools → Network → JS: confirm lazy chunks load on navigation, not all upfront
  4. Check Console for errors
  5. Test any exposeComponent extensions from other Grafana apps

For troubleshooting common issues, see references/troubleshooting.md.


References

FAQ

What does the plugin-bundle-size skill do?

Optimise Grafana app plugin bundle size using React.lazy, Suspense, and webpack code splitting. Use when the user asks to reduce plugin bundle size, optimise module.js, add code splitting, improve initial plugin load performance, split plugin chunks, lazy load plugin pages, or help implement lazy loading in a Grafana app plugin. Triggers on phrases like "optimise plugin bundle size", "module.js is too large", "plugin is slow to load", "code split the plugin", "reduce initial JS payload", or "help me with Suspense in my plugin".

How do I install plugin-bundle-size?

Run npx skills add https://github.com/grafana/skills --skill plugin-bundle-size in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/plugin-bundle-size/ (personal) or .claude/skills/plugin-bundle-size/ (project).

Which AI agents can use plugin-bundle-size?

Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.

Is plugin-bundle-size safe to install?

Security partners have reviewed it: Gen Agent Trust Hub passed; Socket passed; Snyk passed; ZeroLeaks passed. As with any skill, read the SKILL.md and bundled scripts before installing.

Who maintains plugin-bundle-size?

It is published by grafana in grafana/skills. It has 4,183 recorded installs.

More from grafana/skills

View all →

dashboarding

grafana/skills

Build, modify, and ship Grafana dashboards as JSON via the HTTP API — panel types (timeseries / stat / gauge / table / heatmap / logs / traces / node-graph), gridPos 24-column layout, units, thresholds, template + datasource + chained variables, transformations (organize / calculateField / filterByValue), panel + dashboard links with ${field.labels.x} / ${from}, and Loki/Prometheus annotations. Use when scripting dashboard creation, writing the dashboard JSON for a new service, adding a $job dropdown variable, computing an "Error %" column with a transformation, overlaying deploys as annotations, or pushing a dashboard via POST /api/dashboards/db — even when the user says "create a dashboard for this metric", "add a service dropdown", "show errors as percentage", "overlay our deploys", or "export the dashboard JSON" without naming the API or schema. After every API push, verify with the returned version plus a GET on the dashboard UID.

Backend & APIs8.6K installs

promql

grafana/skills

Write, validate, and optimize PromQL for Prometheus / Grafana Mimir / Grafana Cloud Metrics. Covers rate vs irate vs increase, label matchers and regex, sum / avg / topk / by / without aggregation, classic + native histogramquantile, ratios with divide-by-zero guards, absent / changes for staleness, time offsets and predictlinear, recording-rule naming, SLO + burn-rate math, and a cardinality-hunting playbook. Use when writing a metric query, fixing wrong p95s, building an error-budget alert, debugging "query is slow", finding the noisy label that blew up cardinality, or migrating a dashboard query to a recording rule — even when the user says "calculate the error rate", "p99 latency", "sum by service", "why is this query slow", or "what's filling Mimir" without naming PromQL.

Data & Databases8.1K installs

loki

grafana/skills

Grafana Loki log aggregation and LogQL query language. Covers LogQL syntax (log queries, metric queries, label matchers, line filters, parsers: json/logfmt/pattern/regexp/unpack, label filters, lineformat), Loki architecture, log ingestion via Alloy/Promtail/Fluent Bit, structured metadata, and Logs Drilldown. Use when writing LogQL queries, configuring Loki, troubleshooting log pipelines, or analyzing logs.

loki-label-analyzer

grafana/skills

Expert evaluator for Grafana Loki label strategy. Audits, designs, and improves label schemas using cardinality scoring, access-pattern alignment, static vs. dynamic label rules, and consistency checks. Use when the user asks to evaluate, audit, design, or improve a Loki label strategy — or asks why their Loki queries are slow.

Security6.4K installs

opentelemetry

grafana/skills

Instrument any app with OpenTelemetry and ship metrics / logs / traces to Grafana Cloud or self-hosted Mimir / Loki / Tempo / Pyroscope. Covers SDK auto-instrumentation for Go, Java (Grafana JVM agent), Python (opentelemetry-instrument), Node.js, .NET (Grafana.OpenTelemetry), Beyla eBPF for zero-code; Grafana Cloud OTLP gateway + Basic-auth (instanceID + API key, base64); env-var config (OTELEXPORTEROTLP, OTELRESOURCEATTRIBUTES); Alloy / OTel-Collector pipelines; Kubernetes Operator inject-annotations; and head + tail sampling. Use when instrumenting a service, pointing OTLP at Grafana Cloud, switching from Jaeger / Datadog / New Relic, choosing head- vs tail-sampling, or debugging "spans aren't showing in Explore" — even when the user says "auto-instrument my Java app", "send traces to Grafana", "what env vars do I set", "OTLP endpoint", or "Operator inject" without naming OpenTelemetry.

Backend & APIs6.1K installs

grafana-oss

grafana/skills

Configure Grafana OSS — provisions dashboards from YAML, sets up data sources (Prometheus / Loki / Tempo / Pyroscope), writes dashboard JSON with template variables, builds panel queries, assigns built-in roles (Viewer / Editor / Admin / GrafanaAdmin), mints service-account tokens, edits grafana.ini server config, creates annotations, installs plugins via provisioning, and validates each step with a health-check curl. Use when building dashboards, configuring data sources, setting up provisioning YAML, picking a panel type, writing template variables, managing users and roles, configuring SMTP/OAuth in grafana.ini, creating annotations via API, troubleshooting why a provisioned dashboard isn't showing up, or running Grafana OSS locally — even when the user says "set up a Prometheus data source", "provision dashboards from git", "make a service account", or "configure SSO in OSS" without saying "Grafana OSS".

Backend & APIs5.6K installs

Popular frontend & design skills

View category →

setup-matt-pocock-skills

mattpocock/skills

Configure this repo for the engineering skills: set up its issue tracker, triage label vocabulary, and domain doc layout. Run once before first use of the other engineering skills.

Frontend & Design1.1M installs

frontend-design

anthropics/skills

Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.

OfficialFrontend & Design1M installs

hyperframes-animation

heygen-com/hyperframes

All animation knowledge for HyperFrames — atomic motion rules, multi-phase scene blueprints, scene transitions, broader motion-design techniques, AND the seven runtime adapters (GSAP default, plus Lottie, Three.js, Anime.js, CSS keyframes, Web Animations API, TypeGPU). Use for any motion or animation task: pick 2-4 rules and compose, or load a blueprint, or look up runtime-specific API (e.g. GSAP eases / Lottie player / Three.js mixer). Also covers auditing an existing composition's choreography (animation map) and 24 named text-animation effects. HyperFrames-native: single paused timeline, seek-safe, deterministic.

prototype

mattpocock/skills

Build a throwaway prototype to answer a design question. Use when the user wants to sanity-check whether a state model or logic feels right, or explore what a UI should look like.

Frontend & Design999.2K installs

hyperframes-core

heygen-com/hyperframes

The HyperFrames composition contract — build one renderable project. Use for composition structure, the data- timing attributes, class="clip", tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Read before writing composition HTML.

Frontend & Design930.5K installs

vercel-react-best-practices

vercel-labs/agent-skills

React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.

OfficialFrontend & Design843.1K installs