vite
vite copied to clipboard
Support inlining SVG assets
I was asked to open another issue for this.
Describe the bug
Vite doesn't inline svg files when the documentation says it would.
Reproduction
https://bitbucket.org/andylizi/test-vite-svg-inline/
Expected behavior
-
logo.svg
should be inlined, asassetsInlineLimit
claims to inline any static asset files under the default 4kb limit. https://github.com/vitejs/vite/blob/9904cb1a758578a508d1e45717b6293162397bb3/src/node/config.ts#L315-L320
Actual behavior
-
logo.svg
is not inlined.
System Info
-
vite
version: v1.0.0-rc.13 - Operating System: Windows 10 (64-bit)
- Node version: v14.8.0
Related code
https://github.com/vitejs/vite/blob/480367b83a5d418e76a4a6bccd004abb97413c22/src/node/build/buildPluginAsset.ts#L60-L63
Preferred solution
Adding support for svg inlining would be great. Unfortunately extra steps are required to do it properly, as #1197 mentioned: Probably Don’t Base64 SVG and Optimizing SVGs in data URIs.
Alternative solution
Document this behavior in config.ts
so users wouldn't be surprised by this.
Workaround
Rename .svg
to uppercase .SVG
. This isn't ideal but it works for now.
While this is in the process of getting fixed, here is my current patchy solution (leveraging webpack's svg-inline-loader 😅 )
Add svgLoader()
to your plugins array and you're good to go!
import { getExtractedSVG } from "svg-inline-loader"
import type { Plugin } from "rollup"
import fs from "fs"
//TODO: remove this once https://github.com/vitejs/vite/pull/2909 gets merged
export const svgLoader: (options?: {
classPrefix?: string
idPrefix?: string
removeSVGTagAttrs?: boolean
warnTags?: boolean
removeTags?: boolean
warnTagAttrs?: boolean
removingTagAttrs?: boolean
}) => Plugin = (options?: {}) => {
return {
name: "vite-svg-patch-plugin",
transform: function (code, id) {
if (
id.endsWith(".svg")
) {
const extractedSvg = fs.readFileSync(id, "utf8")
return `export default '${getExtractedSVG(extractedSvg, options)}'`
}
return code
}
}
}
While this is in the process of getting fixed, here is my current patchy solution (leveraging webpack's svg-inline-loader 😅 )
Add
svgLoader()
to your plugins array and you're good to go!import { getExtractedSVG } from "svg-inline-loader" import type { Plugin } from "rollup" import fs from "fs" //TODO: remove this once https://github.com/vitejs/vite/pull/2909 gets merged export const svgLoader: (options?: { classPrefix?: string idPrefix?: string removeSVGTagAttrs?: boolean warnTags?: boolean removeTags?: boolean warnTagAttrs?: boolean removingTagAttrs?: boolean }) => Plugin = (options?: {}) => { return { name: "vite-svg-patch-plugin", transform: function (code, id) { if ( id.endsWith(".svg") ) { const extractedSvg = fs.readFileSync(id, "utf8") return `export default '${getExtractedSVG(extractedSvg, options)}'` } return code } } }
THANKS! you are awesome!
While this is in the process of getting fixed, here is my current patchy solution (leveraging webpack's svg-inline-loader )
Add
svgLoader()
to your plugins array and you're good to go!import { getExtractedSVG } from "svg-inline-loader" import type { Plugin } from "rollup" import fs from "fs" //TODO: remove this once https://github.com/vitejs/vite/pull/2909 gets merged export const svgLoader: (options?: { classPrefix?: string idPrefix?: string removeSVGTagAttrs?: boolean warnTags?: boolean removeTags?: boolean warnTagAttrs?: boolean removingTagAttrs?: boolean }) => Plugin = (options?: {}) => { return { name: "vite-svg-patch-plugin", transform: function (code, id) { if ( id.endsWith(".svg") ) { const extractedSvg = fs.readFileSync(id, "utf8") return `export default '${getExtractedSVG(extractedSvg, options)}'` } return code } } }
For now, this does not support svg files in styles.
Hi guys, I am experimenting with package vite-svg-loader to make our SVG icons inline. Although SVG icons have already inline HTML when server-side render, the browser still downloads SVG icons through network when rehydrate. We are trying to improve page speed, so inlining small SVG with HTML instead of making requests to download them is one method that we are experimenting with.
Do you know any package like html-loader for Vite/Rollup ecosystem?
How about something like this?
import logoSvgString from './assets/logo.svg?raw';
const fragment = document.createDocumentFragment();
const logoFragment = document
.createRange()
.createContextualFragment(logoSvgString);
fragment.appendChild(logoFragment);
document.body.appendChild(fragment);
Will not work for the <style>
case though :/
Custom SVG are now supported in unplugin-icons
, which allows them to be very easily inlined (relevant discuccion https://github.com/antfu/unplugin-icons/issues/12)
You can find the documentation here: https://github.com/antfu/unplugin-icons#custom-icons
I actually had to get the inlined svg to use in img src
, so I adapted the plugin by @cslecours to use the same data uri extractor as in the PR; see code below if you are looking for the same solution (note the double quotes around the data uri).
import svgToMiniDataURI from "mini-svg-data-uri";
import type { Plugin } from "rollup";
import fs from "fs";
import { optimize, OptimizeOptions } from "svgo";
type PluginOptions = { noOptimize?: boolean; svgo?: OptimizeOptions };
//TODO: remove this once https://github.com/vitejs/vite/pull/2909 gets merged
export const svgLoader: (options?: PluginOptions) => Plugin = (
options?: PluginOptions
) => {
// these options will always be overridden
const overrideOptions: PluginOptions = {
svgo: {
// set multipass to allow all optimizations
multipass: true,
// setting datauri to undefined will get pure svg
// since we want to encode with mini-svg-data-uri
datauri: undefined,
},
};
options = options ?? overrideOptions;
options.svgo = Object.assign(options.svgo ?? {}, overrideOptions.svgo);
return {
name: "vite-svg-patch-plugin",
transform: function (code, id) {
if (id.endsWith(".svg")) {
const extractedSvg = fs.readFileSync(id, "utf8");
const optimized = options.noOptimize
? extractedSvg
: optimize(extractedSvg, options.svgo).data;
const datauri = svgToMiniDataURI.toSrcset(optimized);
return `export default "${datauri}"`;
}
return code;
},
};
};
(makes using dynamic import such as in this gist really powerful)
EDIT: added optional svgo optimizations
How do you remove those assets from being emitted by Vite? Even with those plugins, the svg are still emitted as external files
Any updates on this? Currently, I have a few websites that load multiple SVG images, which make them load pretty slowly.
There's a fairly dead PR open for it, I think it mainly just needs maintainer approval at this point
Does unplugin-icons
solve you usecase?
See https://github.com/vitejs/vite/issues/1204#issuecomment-920821983
Not nearly as cleanly as just adapting the svg-inline-loader
from webpack, both are hacks for a common use case
How do you remove those assets from being emitted by Vite? Even with those plugins, the svg are still emitted as external files
I wrote a plugin to exclude files ending in .svg to prevent them from emitted. Add to plugin array, working as of 4.0.2
const preventSVGEmit = () => {
return {
generateBundle(opts, bundle) {
for (const key in bundle) {
if (key.endsWith('.svg')) {
delete bundle[key]
}
}
},
}
}
Usage:
plugins: [preventSVGEmit()]
In 2023, what is the recommended solution for this? Is this in scope for vite?
For @nikeee and anyone coming after that, it seems you can append ?inline
, ?url
or ?raw
when importing assets.
So, to get a data64 of an svg you'd go:
import myInlineSvg from './path/to/file.svg?inline';
Docs: https://vitejs.dev/guide/assets.html#explicit-url-imports
@hugoatmooven i think the motivation for this issue is inlining in the sense of the SVG object that can be styled, etc etc, rather than a base64 string
@hugoatmooven so inlining SVGs works now? If so, this issue could be closed, or am I getting something wrong?
?inline
means base64: <img src="data:image/svg+xml;base64,..." />
.
However the goal would be to have <svg ...></svg>
(to be able to style it using CSS, not possible with the img
).
I upgraded to the latest version 4.3.0 but even with adding ?inline the respective SVG doesn't get inlined as an data URI in my (S)CSS.
@madeleineostoja @oliverpool
I think that is also available with ?raw
. In a React project it would look like this:
import mySvgContent from './path/to/file.svg?raw';
function MySvgComponent() {
return <div dangerouslySetInnerHTML={mySvgContent} />
}
@hugoatmooven Well I'll be damned, ?raw
works perfectly to inline raw SVG contents.
I think this issue can (finally) be closed out, or perhaps left open as an FAQ/documentation issue?
@madeleineostoja While the ?raw
trick is nice to have, it only works for the specific use-case where you want to embed the raw SVG directly into HTML and is using JavaScript to generate said HTML. It doesn't work in other (arguably more common) situations, such as <img src="logo.svg"/>
or background-image: url(watermark.svg);
, where data URIs are necessary. And you can't just do "data:image/svg+xml," + mySvgContent
because of URL encoding.
Also preferably this should just work out-of-the-box, like how inlining works for every(?) other format.
i think the motivation for this issue is inlining in the sense of the SVG object that can be styled, etc etc, rather than a base64 string
However the goal would be to have
<svg ...></svg>
(to be able to style it using CSS, not possible with theimg
).
Ah apologies I didn't notice the discussion regarding goals and motivation before.
It'd be great to be able to embed the SVG element into HTML, but that feature feels more like a future expansion to me, rather than the solution to the current problem described in this issue. I feel this way because:
- Sometimes it is not the desireable behavior. For example, it'd be pretty surprising if
<img class="my-logo" src="logo.svg"/>
gets turned into<svg class="my-logo">...</svg>
silently and irrevocably. And as I mentioned before, some use-cases can only use data URIs. - AFAIU, this would need to be implemented in a completely separate way compared to the current asset inlining logic, since it involves special HTML transformation.
- As there're different use-cases, there's no reason we can only have one way of inlining SVGs, and any future implementation of such won't (and shouldn't) conflict with data URIs. The pros and cons of adding that feature, especially the question of whether it was in scope for vite (instead of, like, a plugin), probably need to happen in another discussion.
?inline
means base64:<img src="data:image/svg+xml;base64,..." />
.However the goal would be to have
<svg ...></svg>
(to be able to style it using CSS, not possible with theimg
).
It seems this could be done by appending ?raw&inline
to the SVG file path:
import MyLogo from 'path/to/svg?raw&inline';
MyLogo
will be <svg ...></svg>
. Then you can embed it directly in HTML (Svelte):
{@html MyLogo}
Or if you want to use it for a favicon or <img>
:
<link rel="icon" href="data:image/svg+xml;base64,{btoa(MyLogo)}" type="image/svg+xml" />
<img src="data:image/svg+xml;base64,{btoa(MyLogo)}" alt="" />
Here’s my code: https://github.com/sveltia/sveltia-cms/commit/4dc8c63ec99acdbd4a68cbabbc2a225ce9453a3a All these files are bundled into one single JavaScript file sveltia-cms.js
.
?inline
means base64:<img src="data:image/svg+xml;base64,..." />
.However the goal would be to have
<svg ...></svg>
(to be able to style it using CSS, not possible with theimg
).
Doing ?inline
should emit <img src="data:image/svg+xml;base64,..." />
or <img src="data:image/svg+xml;utf-8,..." />
as per docs. But currently, this doesn't and is broken. It emits <img src="/path/to/image.svg?inline" />
instead. Due to the SVG not being inlined, a seperate network request ist done, creating a visible lag.
I think vite should not do magic and emit <svg>...</svg>
, as this has entirely different semantics and would break stuff.
Also, this approach doesn't work for imports in CSS files (which is what I am using this for):
.a {
background-image: url("./path/to/image.svg?inline");
/*
expected result:
background-image: url("data:image/svg+xml;base64,...");
actual result:
background-image: url("/path/to/image-hash.svg?inline");
*/
}
This is currently broken, too. Probably the logic between the include implementations is shared.
as a workaround for inlining SVGs in CSS files i'm now using the postcss-inline-svg
plugin.
Thanks @oberhamsi for that suggestion! I'll use it now too!
as a workaround for inlining SVGs in CSS files i'm now using the
postcss-inline-svg
plugin.
This is so great! Thank you for this. Out of the box support for PostCSS in Vite is amazing as well. So I added "postcss-inline-svg" as dev dependency, and then created a "postcss.config.cjs" config file ( I had to use .cjs extension) that references the plugin with the usual syntax, for example:
module.exports = {
plugins: {
'postcss-inline-svg': {}
}
}
Then in CSS I load svgs like
background-image: svg-load('./assets/vite.svg');
When built, all the svg are automatically inlined and are not outputted to the dist. Finally!
@madeleineostoja While the
?raw
trick is nice to have, it only works for the specific use-case where you want to embed the raw SVG directly into HTML and is using JavaScript to generate said HTML. It doesn't work in other (arguably more common) situations, such as<img src="logo.svg"/>
orbackground-image: url(watermark.svg);
, where data URIs are necessary. And you can't just do"data:image/svg+xml," + mySvgContent
because of URL encoding.Also preferably this should just work out-of-the-box, like how inlining works for every(?) other format.
For automatic inlining in CSS, see my previous reply. For img src inlining, here is how I do:
- Load with ?raw, for example:
import javascriptLogo from './assets/javascript.svg?raw'
- add a JS function to the main.js:
const svg = (() => {
// Source: https://github.com/tigt/mini-svg-data-uri
// see: https://github.com/tigt/mini-svg-data-uri/issues/24
const reWhitespace = /\s+/g
const reUrlHexPairs = /%[\dA-F]{2}/g
const hexDecode = { '%20': ' ', '%3D': '=', '%3A': ':', '%2F': '/' }
const specialHexDecode = match => hexDecode[match] || match.toLowerCase()
const svgToTinyDataUri = svg => {
svg = String(svg)
if (svg.charCodeAt(0) === 0xfeff) svg = svg.slice(1)
svg = svg.trim().replace(reWhitespace, ' ').replaceAll('"', '\'')
svg = encodeURIComponent(svg)
svg = svg.replace(reUrlHexPairs, specialHexDecode)
return 'data:image/svg+xml,' + svg
}
svgToTinyDataUri.toSrcset = svg => svgToTinyDataUri(svg).replace(/ /g, '%20')
return svgToTinyDataUri
})()
then in the app, when you need to inline a svg in a img src load it with:
<img src="${svg(javascriptLogo)}" class="logo vanilla" alt="JavaScript logo" />