Dynamic SVG Loading Mechanism in Vue-Color-Avatar: A Technical Deep Dive
Vue-Color-Avatar leverages JavaScript's dynamic import() alongside Vite's ?raw query parameter to lazy-load SVG avatar components on demand, reducing initial bundle size while enabling runtime color customization.
The vue-color-avatar library generates customizable avatars by compositing individual SVG widgets—such as faces, ears, and clothing—into a single cohesive image. Understanding its dynamic SVG loading mechanism reveals how modern Vite-based applications can efficiently manage large static asset libraries without bloating the initial JavaScript payload.
Registry of Lazy Imports in dynamic-data.ts
The foundation of the system lies in src/utils/dynamic-data.ts, which exports a centralized registry called widgetData. This object maps each WidgetType and shape combination to a function that returns a dynamic import promise.
// src/utils/dynamic-data.ts
type Data = Readonly<{
[key in `${WidgetType}`]: { [key in string]: () => Promise<any> }
}>;
const widgetData: Data = {
[WidgetType.Face]: {
[FaceShape.Base]: () => import(`../assets/widgets/face/base.svg?raw`),
},
[WidgetType.Ear]: {
[EarShape.Attached]: () => import(`../assets/widgets/ear/attached.svg?raw`),
[EarShape.Detached]: () => import(`../assets/widgets/ear/detached.svg?raw`),
},
// Additional widget types (Eyes, Beard, Clothes, etc.) follow the same pattern
};
Each entry uses a lazy-import function that executes only when called. The ?raw suffix instructs Vite to treat the SVG file as a plain text string rather than a processed component, returning the raw markup via the module's default export.
Runtime Assembly in VueColorAvatar.vue
The component src/components/VueColorAvatar.vue orchestrates the loading process. Inside a watchEffect, the component reacts to changes in avatar options and triggers the dynamic resolution sequence.
Sorting by zIndex
First, the component sorts active widgets according to their zIndex values to ensure proper layering:
const sortedList = Object.entries(avatarOption.value.widgets).sort(
([, prev], [, next]) => {
const ix = prev.zIndex ?? AVATAR_LAYER[prev.shape]?.zIndex ?? 0;
const iix = next.zIndex ?? AVATAR_LAYER[next.shape]?.zIndex ?? 0;
return ix - iix;
}
);
Constructing Import Promises
Then it constructs an array of promises that invoke the registry functions:
const promises = sortedList.map(async ([widgetType, opt]) => {
if (opt.shape !== NONE && widgetData?.[widgetType]?.[opt.shape]) {
return (await widgetData[widgetType][opt.shape]()).default;
}
return '';
});
The check against NONE prevents loading when a widget type is disabled.
SVG Processing and Color Injection
Once resolved, the raw SVG strings undergo processing to inject dynamic colors and remove redundant root elements. The Promise.all resolution handles placeholder substitution and markup extraction:
const svgRawList = await Promise.all(promises).then((raw) => {
return raw.map((svgRaw, i) => {
const [widgetType, widget] = sortedList[i];
let fill = widget.fillColor;
/* Apply skin-color inheritance for ears */
if (widgetType === WidgetType.Face) skinColor = fill;
if (skinColor && widgetType === WidgetType.Ear) fill = skinColor;
const content = svgRaw
.slice(svgRaw.indexOf('>', svgRaw.indexOf('<svg')) + 1)
.replace('</svg>', '')
.replaceAll('$fillColor', fill || 'transparent');
return `<g id="vue-color-avatar-${sortedList[i][0]}">${content}</g>`;
});
});
The code strips the outer <svg> tags from each component (keeping only the inner content), replaces $fillColor placeholders with actual hex values, and wraps each part in a <g> element with a unique ID.
Final Composition and Rendering
The processed fragments are assembled into a single parent SVG structure:
svgContent.value = `
<svg width="${avatarSize.value}" height="${avatarSize.value}"
viewBox="0 0 ${avatarSize.value / 0.7} ${avatarSize.value / 0.7}"
preserveAspectRatio="xMidYMax meet"
fill="none" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(100, 65)">
${svgRawList.join('')}
</g>
</svg>`;
This final string is rendered via v-html, resulting in a composite avatar containing only the specific widgets selected by the user.
Why This Architecture Works
The dynamic SVG loading mechanism provides three key advantages:
- Code-splitting: Vite generates separate chunks for each SVG file. The browser fetches only chunks corresponding to active avatar shapes, significantly reducing initial load time.
- Raw string access: The
?rawquery parameter enables string manipulation before DOM insertion, allowing runtime color substitution viareplaceAlloperations. - Type-safe registry: The
widgetDatastructure uses TypeScript mapped types to ensure only valid WidgetType and shape combinations can be registered.
Extending the System: Adding a New Widget
To add a "Hat" widget, create the SVG file and extend the registry:
- Create
src/assets/widgets/hat/fedora.svgcontaining$fillColorplaceholders. - Register the lazy import in
dynamic-data.ts:
[WidgetType.Hat]: {
[HatShape.Fedora]: () => import(`../assets/widgets/hat/fedora.svg?raw`),
},
- Include the widget in avatar options. The component automatically loads
fedora.svgonly when this configuration is active, maintaining the lazy-loading behavior.
Summary
- The dynamic SVG loading mechanism centers on a registry pattern in
src/utils/dynamic-data.tsthat maps widget types to Vite dynamic imports using the?rawsuffix. - Runtime assembly occurs in
src/components/VueColorAvatar.vue, where promises resolve to raw SVG strings that are processed, colored, and composited into a single avatar image. - The system leverages code-splitting to load only required assets, reducing initial bundle size while supporting unlimited widget expansions.
- Color customization happens via string replacement of
$fillColorplaceholders before DOM insertion, enabling dynamic theming without multiple SVG variants.
Frequently Asked Questions
How does Vite's ?raw parameter affect the import?
Vite's ?raw query parameter instructs the bundler to import the file contents as a plain text string rather than as a parsed module or component. According to the vue-color-avatar source code, this allows the application to receive the SVG markup directly via .default, enabling string manipulation methods like replaceAll for color injection before inserting the content into the DOM.
What prevents all SVGs from loading at once?
The lazy-evaluation pattern used in widgetData ensures that import functions remain unexecuted until explicitly called by the component's watchEffect. Each registry entry is a function returning import(), not the import itself, so Vite can code-split each SVG into separate chunks that fetch only when the user selects specific avatar options.
How does the system handle widget layering order?
The component sorts widgets using zIndex values defined in AVATAR_LAYER constants before constructing the import promises. This guarantees that facial features render in the correct sequence (e.g., ears behind faces), regardless of the order in which the asynchronous imports resolve.
Is this dynamic loading approach type-safe?
Yes. The widgetData registry uses TypeScript's mapped types ([key in \${WidgetType}`]) to enforce that only valid widget types and shapes can be registered. This compile-time checking ensures that widgetData[widgetType][opt.shape]` access remains safe, preventing undefined function calls during the dynamic import resolution phase.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →