jamesmartin / inline_svg

Embed SVG documents in your Rails views and style them with CSS
MIT License
716 stars 73 forks source link

Reduce repeated SVG size with <use> tag #124

Open ryanb opened 3 years ago

ryanb commented 3 years ago

If I have a list of a hundred items which all reference the same inline_svg_tag image, the content is duplicated for each item. It's possible to reduce the generated HTML by inlining the SVG once and referencing it in each item using the \<use> tag.

Is this something that fits in the scope of inline_svg? I understand if not, but it seems like a nice feature.

jamesmartin commented 3 years ago

👋 thanks for opening this issue, @ryanb. ✨

If I have a list of a hundred items which all reference the same inline_svg_tag image, the content is duplicated for each item.

Do you have a (simplified) example? I would like to understand how the list of items is constructed, the structure of the SVG and the surrounding HTML document if possible. 🤔

Is this something that fits in the scope of inline_svg? I understand if not, but it seems like a nice feature.

I'm not 100% sure if this fits the scope of inline_svg but I'm hoping some example code will help to clarify that a little.

ryanb commented 3 years ago

@jamesmartin Sure, something along these lines:

<ul>
<% 100.times do %>
  <li><%= inline_svg_tag "handle.svg" %> Item</li>
<% end %>
</ul>

Here the content of the handle.svg is inlined for every list item. If it's a complex SVG this could result in significantly larger HTML.

It's possible to reference part of another SVG using an id like this:

<svg style="display: none">
  <symbol id="handle-icon">...</symbol>
</svg>
<ul>
<% 100.times do %>
  <li><svg><use href="#handle-icon" /></svg> Item</li>
<% end %>
</ul>

This way the SVG content is outside of the repeating content.

This is similar to the first attempt taken by GitHub mentioned in this blog post. Looks like they had cross-domain issue preventing them from doing this, but many don't have that issue.

Perhaps it's not worth the hassle of referencing external SVG icons since most of them are small. Just an idea. :)

jamesmartin commented 3 years ago

@ryanb thanks for the examples, that does seem like a useful feature, especially if there is no cross-domain problem in your use-case.

Based on your second example, which is no doubt simplified for clarity, how do you imagine the inline_svg API would work? The simplest thing I can of is to pass the collection to inline_svg, which would render the SVG document first, setting the display:none style (presumably), and then yield each item of the collection to the block, like this:

<ul>
  <%= (inline_svg_tag "handle.svg", with_use_tag: 100.times) do |item| %>
    <li><svg><use href="#handle-icon" /></svg> <%= item %></li>
  <% end %>
</ul>

This assumes the caller would assemble their own slim SVGs in the block, with the relevant <use> tag. I'm not sure if this actually buys you much though. I feel like I'm still missing something here. 🤔

ryanb commented 3 years ago

how do you imagine the inline_svg API would work?

@jamesmartin good question. In my example I included the svg on the same page but I think it would be better done through a separate URL which could join multiple svgs together, perhaps join everything within a directory similar to how a sprite library works.

# app/assets/images/icons/handle.svg
# app/assets/images/icons/login.svg
# app/assets/images/icons/profile.svg
# app/assets/images/icons/settings.svg

Behind the scenes this would generate an app/assets/images/icons.svg file (maybe through a configuration) which would combine all of the files in the directory under one svg and auto-generate a unique id for each one based on the filename.

<svg xmlns="http://www.w3.org/2000/svg">
  <symbol id="handle" viewBox="0 0 14 14">...</symbol>
  <symbol id="login" viewBox="0 0 14 14">...</symbol>
  <symbol id="profile" viewBox="0 0 14 14">...</symbol>
  <symbol id="settings" viewBox="0 0 14 14">...</symbol>
</svg>

Then you could do <%= inline_svg_use_tag "icons/handle.svg" %> which could output something like this:

<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><use href="assets/icons.svg#handle" /></svg>

The icons.svg file could be cached by the browser similar to images so we have all icons under one request and the generated HTML is very slim.

I understand if this is outside the scope of inline_svg since it involves some asset pipeline magic. Would be cool to have though.

Update: Looks like there are a number of solutions already out there to help with this.

jamesmartin commented 3 years ago

@ryanb thanks for the extra detail, I understand what you're getting at now.

I understand if this is outside the scope of inline_svg since it involves some asset pipeline magic. Would be cool to have though.

inline_svg does have at least a superficial dependency on the asset pipeline (to find assets) and I'm not against this as a feature. Unfortunately I don't think I'm going to have the bandwidth to work on something like this any time soon. If you, or anybody else, wants to spike this out I'd be happy to help design the API and get PRs merged etc.

tleish commented 3 years ago

I'm interested in this also, although I see the API as an "inline" solution (not assets pipeline). Here's what I'm thinking.

  1. Add per request caching to getting svg files (good optimization to add, regardless of use tag.)
module Helpers
  def inline_svg_tag(filename, transform_params={})
    with_asset_finder(InlineSvg.configuration.asset_finder) do
-     render_inline_svg(filename, transform_params)
+     cache_inline_svg(filename, transform_params)
    end
  end

+ def cache_inline_svg(*args)
+   @svg_files ||= {}
+   @svg_files[args] ||= render_inline_svg(*args)
+ end
...
  1. Look for use flag or use_id flag. SVG use requires a unique id per svg sprite, so it would be a great developer experience to auto-create a unique id if they did not specify one. However, use their id if they did specify one.

  2. inline_svg_tag with use param returns:

    <svg xmlns="http://www.w3.org/2000/svg">
     <use href="#inline-svg-aircraft-a4zde5d" x="20" fill="white" stroke="red"/>
    <svg>
  3. Finally, a helper method which combines the unique @svg_files into a single svg sprite.

    <%= inline_svg_sprites_tag, style: 'display: none;' %>

    or

    <% content_for :svg_sprites do %>
       <%= inline_svg_sprites_tag %>
    <% end %>

    which renders something like

    <svg style="display:none;">
     <symbol id="inline-svg-aircraft-a4zde5d" viewBox="0 0 62 51">
       <path fill="#000000" d="M38.9872..."></path>
     </symbol>
     <symbol id="inline-svg-attachment-de5da4z" viewBox="0 0 60 64">
       <path fill="#000000" d="M15.9264..."></path>
     </symbol>
     <symbol id="inline-svg-brush-hasd98w" viewBox="0 0 62 62">
       <path fill="#000000" d="M7.8416..."></path>
     </symbol>
     <symbol id="inline-svg-camer-8hx9rf" viewBox="0 0 64 52">
       <path fill="#000000" d="M32,19.2 ..."</path>
     </symbol>
    </svg> 

This would certainly speed up pages with lots of SVG files.

tleish commented 3 years ago

FYI, others are interested in this also.

see: https://twitter.com/_swanson/status/1336783159460028421