plugin-bundle-size

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Grafana plugin bundle size optimisation

Grafana插件包大小优化

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.
module.js
是每个Grafana应用插件的渲染阻塞入口文件。它的体积越小,插件对Grafana整体启动时间的影响就越小。一个拆分良好的插件,其
module.js
体积应控制在200 KB以内,且仅包含懒加载包装器——所有功能代码都按需加载。
目标: 总共15–25个JS代码块。数量过少意味着拆分不足;数量过多(50+)则属于过度设计。

Risk levels

风险等级

Not all splitting opportunities carry the same risk. Apply them in this order:
LevelWhatRiskImpact
Safe
module.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%+.

并非所有拆分机会的风险都相同,请按以下顺序实施:
等级内容风险影响
安全
module.tsx
懒加载包装器(优先级1)
极低——无行为变更最高——module.js体积减少90%以上
安全路由级
lazy()
(优先级2)
低——每个路由独立封装高——每个路由对应一个代码块
安全扩展
lazy()
(优先级3)
低——扩展相互隔离中——每个扩展对应独立代码块
中等组件注册表/标签面板(优先级4)中——需验证Suspense的放置位置中——进一步拆分大型页面
请勿触碰第三方库(
@grafana/scenes
@reduxjs/toolkit
不适用不适用——webpack会自动拆分这些库
请勿触碰多文件共享的工具组件(Markdown、Spinner)高维护成本,调用点众多低——已在共享第三方代码块中
若有疑问,完成优先级2的操作即可。仅路由拆分通常就能使
module.js
体积减少95%以上。

Step 1: Add bundle size CI reporting (recommended)

步骤1:添加包大小CI报告(推荐)

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):
yaml
undefined
添加
grafana/plugin-actions/bundle-size
动作,在每个PR上自动生成包大小对比评论。该动作会发布一个表格,展示入口文件大小变化、文件数量差异以及对总包体积的影响。
根目录插件(插件位于仓库根目录):
yaml
undefined

.github/workflows/bundle-size.yml

.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:

```yaml
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.

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

**子目录插件**(例如单仓库中的`plugin/`目录):

该动作的安装步骤在仓库根目录执行,无法在子目录中找到`yarn.lock`。可通过自行安装依赖并创建根目录符号链接来解决:

```yaml
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
工作原理: 推送至main分支时,构建并上传基准产物。PR时,与基准产物对比并发布差异评论。使用
workflow_dispatch
生成首个基准产物。

Step 2: Detect plugin context

步骤2:检测插件上下文

bash
undefined
bash
undefined

Confirm this is an app plugin (type: "app" — datasource/panel plugins have different needs)

确认这是应用插件(type: "app" — 数据源/面板插件需求不同)

jq -r '"(.id) — (.type)"' src/plugin.json
jq -r '"(.id) — (.type)"' src/plugin.json

Locate the entry point

定位入口文件

ls src/module.ts src/module.tsx 2>/dev/null
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.

---
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

记录基准数据。未拆分的插件通常`module.js`体积为1–3 MB,且无其他JS代码块。

---

Step 3: Check and update create-plugin

步骤3:检查并更新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.
bash
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-merge
s the base config, review the merge for conflicts.

@grafana/create-plugin
工具控制
.config/webpack/
.config/jest/
及其他构建脚手架。更新该工具通常能解锁更快的SWC编译和更优的代码块输出。
bash
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
更新后,查看差异(尤其是
.config/webpack/webpack.config.ts
)并运行测试构建。若插件有顶层
webpack.config.ts
通过
webpack-merge
合并基础配置,需检查合并是否存在冲突。

Step 4: Analyse the codebase — find what to split

步骤4:分析代码库——确定拆分对象

Do not start implementing until you have read all of these.
bash
undefined
在开始实施前,请务必阅读完所有内容。
bash
undefined

Entry point — look for direct (non-lazy) imports of App, ConfigPage, exposeComponent targets

入口文件——查找对App、ConfigPage、exposeComponent目标的直接(非懒加载)导入

cat src/module.ts 2>/dev/null || cat src/module.tsx
cat src/module.ts 2>/dev/null || cat src/module.tsx

Root App component — look for direct page/route imports that should be lazy

根App组件——查找应懒加载的直接页面/路由导入

cat src/App.tsx src/components/App.tsx src/feature/app/components/App.tsx 2>/dev/null | head -80
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
grep -r "exposeComponent|addComponent|addLink" src/ --include=".ts" --include=".tsx" -n

Exported side-effect singletons (Faro, analytics) — must be extracted before splitting

导出的副作用单例(Faro、分析工具)——拆分前必须提取

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
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.

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

**核心规则:** 如果文件被`module.ts`直接导入(即使是间接导入),它会被包含在`module.js`中。所有可从懒加载边界访问的内容会成为独立代码块。

---

Step 5: Implement splits — in priority order

步骤5:按优先级实施拆分

Named vs default exports:
React.lazy()
requires a
default
export. Most Grafana plugin components use named exports — use
.then()
to re-map:
ts
// Named export
const LazyMyComp = lazy(() => import('./MyComponent').then(m => ({ default: m.MyComponent })));
// Default export
const LazyMyComp = lazy(() => import('./MyComponent'));
命名导出与默认导出:
React.lazy()
要求使用
default
导出。大多数Grafana插件组件使用命名导出——可使用
.then()
重新映射:
ts
// 命名导出
const LazyMyComp = lazy(() => import('./MyComponent').then(m => ({ default: m.MyComponent })));
// 默认导出
const LazyMyComp = lazy(() => import('./MyComponent'));

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

优先级1:module.tsx(影响最大,务必首先执行)

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()
:
tsx
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.

若入口文件为
module.ts
,重命名它:
git mv src/module.ts src/module.tsx
使
module.tsx
仅通过
lazy()
导入功能代码:
tsx
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 — 编译时会被移除
import type { JsonData } from './features/app/state/slice';

// 懒加载初始化Faro——将@grafana/faro-react排除在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 });
关键细节:
  • 使用
    import type
    导入props可防止webpack将该导入纳入即时加载包
  • 若App使用
    AppRootProps<JsonData>
    ,请使用
    new AppPlugin<JsonData>()
    ——若无泛型,
    setRootPage()
    类型将不匹配
  • 移除任何
    App as unknown as ComponentClass<AppRootProps>
    类型转换——懒加载包装器是有效的函数组件
预期影响:
module.js
体积从MB级降至约50–200 KB。
单例(如Faro):
module.ts
包含
export const faro = initializeFaro()
,请勿将其保留为顶层导入。将其提取至
src/faro.ts
,更新所有内部导入路径从
'*/module'
改为
'*/faro'
,然后使用上述动态
initFaro()
模式。

Priority 2: Route-based splitting in App.tsx

优先级2:App.tsx中的路由级拆分

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:
tsx
// 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 })));
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;
绕过桶文件:
import()
中指向实际组件文件,而非重新导出多个内容的
index.ts
桶文件:
tsx
// 有风险——桶文件可能引入其他大型模块
const Catalog = lazy(() => import('features/catalog'));
// 更优——仅加载Catalog相关代码树
const Catalog = lazy(() => import('features/catalog/Catalog').then(m => ({ default: m.Catalog })));

Priority 3: Extension components

优先级3:扩展组件

Each extension should
export default
its component. Use
fallback={null}
for extensions that load quickly:
tsx
// 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:
tsx
const HeavyInner = lazy(() => import('components/features/HeavyInner'));
export function MyExtension() {
  return <Suspense fallback={<LoadingPlaceholder text="" />}><HeavyInner /></Suspense>;
}
每个扩展应
export default
其组件。对于加载速度快的扩展,使用
fallback={null}
tsx
// src/extensions/MyExtension.tsx
export default function MyExtension(props: MyExtensionProps) {
  return <AppProviders><MyExtensionContent {...props} /></AppProviders>;
}
精准拆分: 若扩展包装器必须在
module.tsx
中即时加载,可懒加载其渲染的大型组件:
tsx
const HeavyInner = lazy(() => import('components/features/HeavyInner'));
export function MyExtension() {
  return <Suspense fallback={<LoadingPlaceholder text="" />}><HeavyInner /></Suspense>;
}

Priority 4: Component registries and tab panels

优先级4:组件注册表和标签面板

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.
tsx
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.

对于包含React组件的对象数组(如标签面板),懒加载每个条目。关键: 确保组件渲染位置存在
<Suspense>
边界。
tsx
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 },
];

// 在渲染活动标签的父组件中:
<Suspense fallback={<LoadingPlaceholder text="" />}>
  {ActiveTab && <ActiveTab />}
</Suspense>
对于数据源插件
setConfigEditor
setQueryEditor
VariableSupport
AnnotationSupport
),请参考references/datasource-plugins.md

Step 6: Group related chunks if over-splitting

步骤6:若过度拆分则合并相关代码块

If the build produces more than ~25 JS files, use webpack magic comments:
tsx
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.

若构建生成超过约25个JS文件,使用webpack魔法注释:
tsx
const FleetList   = lazy(() => import(/* webpackChunkName: "fleet" */ '../pages/FleetList'));
const FleetDetail = lazy(() => import(/* webpackChunkName: "fleet" */ '../pages/FleetDetail'));
每个逻辑功能区域使用一个
webpackChunkName
。请勿合并不相关页面。

Step 7: Measure and verify

步骤7:测量并验证

bash
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
bash
undefined
bash
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
指标目标
module.js
体积
< 200 KB
JS代码块总数15–25
单个最大代码块< 1 MB
bash
undefined

Analyse bundle composition if a chunk is unexpectedly large

若某个代码块体积异常大,分析包组成

npx webpack-bundle-analyzer dist/stats.json 2>/dev/null

---
npx webpack-bundle-analyzer dist/stats.json 2>/dev/null

---

Step 8: Test the running plugin

步骤8:测试运行中的插件

  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.

  1. 在Grafana实例中打开插件
  2. 导航至所有路由——每个路由会触发新代码块下载
  3. 开发者工具 → 网络 → JS:确认懒加载代码块在导航时加载,而非全部预先加载
  4. 检查控制台是否有错误
  5. 测试其他Grafana应用调用的
    exposeComponent
    扩展
如需排查常见问题,请参考references/troubleshooting.md

References

参考资料