So far our focus has been on raster image formats—and for good reason. Raster formats are the dominant image format on the Web. They’re capable of representing highly detailed and photographic images, whereas vector formats are not. Combine that with the large ecosystem of tooling and editors supporting them, and it’s little wonder that they’ve reigned supreme.
However, raster images are not without limitations. They don’t scale well, and there is an almost linear relationship between display size and file size. Both of these issues have become glaringly obvious thanks to the rapid increase in the diversity of devices accessing the Web. Confronted with the variety of screen sizes being used, web designers and developers have become painfully aware of these issues. There have been two major outcomes of this:
New responsive image standards have evolved.
There has been an increased interest in vector formats, most notably the Scalable Vector Graphics (SVG) format.
We will be discussing the responsive image standards in Chapter 11, Responsive Images. In this chapter, let’s take a look at how vector formats overcome the limits of raster formats, and how and when to use them.
As we saw in Chapter 2, raster images are composed of a matrix of pixels. This matrix matches the dimensions of the actual image, and each cell represents a single pixel of the image. While this matrix enables raster formats to represent detailed photographic images, it also causes issues when trying to scale raster images.
If a browser or other application needs to scale a raster image beyond the size of that matrix, it needs to create new pixels. For example, scaling a 100x100-pixel image to 120x120 means the browser needs color data for an additional 4,400 pixels (120x120–100x100) that are not represented in the original matrix.
Lacking the proper information for these additional pixels, the browser does the next best thing: it guesses. The browser looks at the surrounding pixels to get an idea of what color values are present, and then uses that information to figure out approximately what color values these additional pixels should contain. The result of all these extra pixels, with their approximated values, is that a scaled raster image will appear blurry and full of digital artifacts.
If a developer wants to serve a raster image intended to be displayed at a variety of sizes without scaling artifacts, he or she will need to produce the image at a number of different dimensions and serve them using the appropriate responsive standards (discussed in Chapter 11). This helps to minimize the scalability issues, but it highlights another limitation of raster formats: the almost linear relation between image dimensions and file size.
Imagine you were going to include a simple square image on your site. The square would need to be displayed at 100x100 pixels on the smallest screen, 700x700 pixels on the largest, and 400x400 pixels somewhere in between. Even though the image is not complex at all, the way raster formats work means that all pixels will need to be processed, compressed, and outputted to the final file. This means our 700x700 version of the square is likely to be significantly heavier than our 100×100 pixel version of the square—it has to store 490,000 pixels (700×700) of data compared to 10,000 pixels of data (100×100) for the smaller image.
Vector formats help to address these limitations by describing how an image should be constructed, rather than storing all the pixel data. Vector images comprise a series of points, lines, curves, shapes, and colors that are described by mathematical expressions. This allows the client to calculate how the image should be displayed, maintaining these basic expressions no matter the size of the image ultimately being displayed.
Let’s go back to our square. Using a vector format like SVG, the instruction for building the square would look like this:
<svg
xmlns=
"http://www.w3.org/2000/svg"
width=
"100"
height=
"100"
viewBox=
"0 0 100 100"
>
<rect
height=
"100"
width=
"100"
fill=
"#f00"
/>
</svg>
If the specific markup there doesn’t make sense to you yet, don’t worry—we’ll get to the basics of the SVG format in a minute. For now, it’s enough to know that these three lines of code describe a perfect square, filled in with the color red. This same markup accurately describes the square no matter the resolution; whether displayed at 100x100 or 700x700, these commands are all the client needs to determine just how to draw the image—no difference in file size necessary.
There’s no such thing as a free lunch, though—there is a tradeoff being made here. While using markup to describe the content of an image can greatly reduce the file size, it does require the client to do more work processing that information and drawing it out to the screen. Typically, this process involves the data in the SVG being software rendered to a bitmap (rasterized), and then uploaded to the GPU. The more complex the image, the more instructions the software needs in order to perform rasterization. The more instructions, the longer it will take to process and follow them all. This is why you will frequently see poor performance on older browsers, particularly on low-powered mobile devices, for SVG. It’s a complex task that can tax the device.
Thankfully, this situation is improving. Browsers are continuing to evolve and improve the efficiency of how they display and process SVG images, eliminating the bulk of the on-device overhead. Still, it’s a good idea to remember that the more complex your SVG image, the more work you’ll be requiring the client to do in order to display it. That complexity also means a heavier file size. As a result, vector formats should be used primarily on simple images: those with a limited number of colors and shapes.
On the Web if you want to use a vector format, it’s going to be SVG. There aren’t any other contenders with any semblance of cross-browser support. Before we get into the specifics of how to optimize SVG files for performance, let’s spend a few pages getting to understand a few core concepts of the format.
It’s beyond the scope of this chapter to try to provide a comprehensive, detailed resource for the SVG format—we’re primarily focused on the basics and the performance impacts. If you’d like to dig deeper, Sara Souedian has done a tremendous job writing detailed posts on her blog.
If you’re just starting to get familar with the SVG format, it can be useful to remember that it’s markup, like HTML. In HTML, a root element (<html>
) and any number of other elements enable you to define particular sections of your page. A distinct paragraph goes in the <p>
element. A table is marked up with the <table>
element.
SVG works similarly. There is a root element (<svg>
) and a number of different core elements. The difference is that in the case of SVG, those core elements are describing shapes and lines. Just as with elements in an HTML page, we can style the individual elements within an SVG document using CSS and interact with them using JavaScript. This not only enables far more granular control over how the individual pieces constituting an SVG element are displayed, but also allows you to animate or otherwise alter these elements.
SVG elements are positioned based on a coordinate system. The top-left corner, for example, has the coordinates (0,0). Any element positioned within the document is measured from that top-left corner. If you’re thinking that sounds similar to what you learned in school about graphs, you’re right—with one wrinkle. In SVG, the x
attribute pushes you to the right and the y
attribute pushes you toward the bottom—the opposite of what you may be used to.
Consider, again, a simple red square:
<rect
x=
"10"
y=
"10"
height=
"100"
width=
"100"
fill=
"#f00"
/>
This element would result in a square that starts 10 px from the left and 10 px from the top, then spans 100 px to the right and 100 px down. Where things get interesting is when you consider the canvas that the coordinate system relates to.
In the context of a web page, there are two variables in terms of size: the dimensions of the page itself, and the dimensions that can be viewed at a single moment in time. For example, you may have a web page that, with all content and images loaded, is 3,000 px high. Yet the browser window itself may only show 800 px vertically at a single time. Similarly, your page may be wider than the width of the browser. In either case, some content will not be visible—it’ll be cut off from view.
The same is true of SVG. You have a canvas that can be thought of as infinite in height and width, and you have a viewport that defines what part of that SVG canvas is actually visible—anything that lies outside of that viewport area is cut off.
To define the viewport, you apply the width
and height
attributes to the root <svg>
element:
<svg
xmlns=
"http://www.w3.org/2000/svg"
width=
"100"
height=
"100"
>
<!-- super exciting SVG content to be drawn -->
</svg>
This example establishes a viewport of 100x100 pixels. In addition, by establishing the viewport, we’ve also initialized two different coordinate systems: the initial viewport coordinate system and the initial user coordinate system.
The initial viewport coordinate system is based on the viewport. Its origin is the top-left corner of the viewport—point (0,0). The initial user coordinate system is based on the SVG canvas. Initially, it’s identical to the viewport coordinate system and has the same origin point of (0,0). The primary difference between the two is that the user coordinate system can be altered via the viewBox
attribute.
The viewBox
attribute accepts a value made up of four parameters: min-x
, min-y
, width
, and height
. The first two parameters, min-x
and min-y
, set the upper-left corner of the viewbox, while the final parameters, width
and height
, determine the dimensions. The viewBox
attribute is crucial—without it browsers will not scale SVG elements.
If we revisit our example from before, we could set the viewBox
to be identical to the SVG viewport by applying the viewBox
attribute as follows:
<svg
xmlns=
"http://www.w3.org/2000/svg"
width=
"100"
height=
"100"
viewBox=
"0 0 100 100"
>
<!-- super exciting SVG content to be drawn -->
</svg>
That in itself is not particularly interesting. Where it gets fun is when we start to alter the viewBox
so that it has different dimensions from the viewport or shift it from the top-left corner a bit.
For example, let’s alter the viewBox
to be a quarter of the size of the viewport itself:
<svg
xmlns=
"http://www.w3.org/2000/svg"
width=
"100"
height=
"100"
viewBox=
"0 0 25 25"
>
<!-- super exciting SVG content to be drawn -->
</svg>
In this example, because we’ve kept min-x
and min-y
set to “0” (the first two values of the viewBox
attribute), the top-left corner of the viewBox
is still the same point as the viewport: (0,0). However, the dimensions of the viewBox
itself are only a quarter of the size of the full viewport.
You can imagine the viewBox
in this case to be an instruction to the client to zoom in on a certain portion of the viewport area. In this case, the selected area will be the first 25x25 area, starting at the top-left corner. Anything that is part of the SVG element that does not lie within the viewBox
will be ignored.
With that 25x25 area selected, the next thing that happens is that the client will zoom in on it—scaling it up until the area within the viewBox
is stretched to fill the full 100x100-pixel viewport of the SVG itself (see Figure 6-1). What we’ve essentially done here is select a subset of the SVG image and zoom in by a factor of four.
We’ve also changed the user coordinate system. It no longer matches the viewport coordinate system. Instead, one user coordinate unit is equivalent to four units in the viewport coordinate system (because we zoomed in by a factor of four).
Sara Soueidan has compared this to Google Maps. You can choose a specific region within a given map, and Google Maps will zoom in on it. The rest of the map is still there, it’s just not visible as it extends beyond the viewport. This is exactly what happens here: the rest of the content within the SVG file still exists, it’s just not visible. It’s cropped out.
Just as you’ll often want to keep the height
and width
attributes around, you’ll almost certainly want to make sure the viewBox
attribute is present and accounted for. If a browser sees an SVG image without a defined viewBox
, it will not scale that image—effectively defeating one of the primary reasons why the SVG format exists.
Most drawing within an SVG image is done by including a number of different elements that correspond to various shapes. The basic shapes included within SVG are listed in Table 6-1.
Basic shape | Corresponding element | Example |
---|---|---|
Rectangle |
|
|
Circle |
|
|
Ellipse |
|
|
Straight line |
|
|
Group of connected lines |
|
|
Polygon |
|
|
In addition to the basic shapes, there is also the <path>
element. If using basic shapes is roughly the equivalent of tracing predefined stencils on a paper, then the <path>
element is the equivalent of handing you a pencil and letting you draw whatever you wanted. Because of this, the <path>
element is incredibly powerful and expressive. That power comes at the cost of complexity, though.
For example, if you wanted to draw a 100x100-pixel red square using the <rect>
element, the code would look something like this:
<svg
xmlns=
"http://www.w3.org/2000/svg"
width=
"100"
height=
"100"
viewBox=
"0 0 100 100"
>
<rect
height=
"100"
width=
"100"
fill=
"#f00"
/>
</svg>
In contrast, accomplishing the same thing with the <path>
element would involve quite a bit more information:
<svg
xmlns=
"http://www.w3.org/2000/svg"
width=
"100"
height=
"100"
viewBox=
"0 0 100 100"
>
<path
d=
"M 0 0 H 100 V 100 H 0"
fill=
"#f00"
></path>
</svg>
Let’s break down that <path>
description attribute (d
) to see exactly what’s happening here:
M 0 0
The M
represents the “MoveTo” command and moves to a specific point within the coordinate system. It’s necessary at the start of a path description to establish the starting point. Here, we’ve specified that the starting point should be the top-left corner of the user coordinate system—point (0,0).
H 100
H
is a shortcut for drawing a horizontal line. In this case, we’re telling the client to draw a horizontal line 100 px wide, starting at the current position (which we defined using the initial “MoveTo” command).
V 100
At this point, we’ve drawn a 100 px horizontal line from point (0,0), bringing us to point (100,0). Now we need to tell the client to draw a vertical line, which we do using the V
command. In this example, we’ve just told the client to draw a 100 px vertical line, bringing us to point (100,100).
H 0
We’re almost there! All we need to do now is tell the client to draw one more horizontal line, back to point (0,100). We don’t need to tell it to close the shape—it will connect the final point to the origin by default.
With the <rect>
element, we didn’t have to go through the process of explaining each individual command. That’s because the <rect>
element is essentially a shortcut—it understands the basic process of drawing out a rectangular shape, so all it needs are the starting point and the dimensions.
This is true of each basic shape: they’re essentially recipes. They know the steps needed to construct a very specific type of shape and, as a result, don’t need as many detailed instructions as required with the <path>
element.
Our square example wasn’t too unwieldy, but that’s not always the case: the more complex the path you are describing, the more instructions you’ll need to provide. This can lead to significant bloat within the file. It also makes the client work harder to rasterize those paths. Basic shapes aren’t just shortcuts for you, the person coding the SVG files—they’re shortcuts that the browser can take as well.
For this reason, whenever possible, you’ll want to favor using basic shapes for drawing purposes—lightening the load over the network as well as the load on the client to display the image once it has been downloaded. Save the <path>
element for more complex shapes and use them sparingly.
This isn’t necessarily a hard and fast rule—it would be boring if it were that easy. There are exceptions. The most typical are the polygon and polyline shapes, both of which can get rather complicated for more complex shapes. At times in those situations, the <path>
element may actually be less complicated and bloated. As with anything in terms of performance, be sure to measure the impact to ensure you’re getting the results you’re after.
One of the really nice features of SVG is that you can group and reuse elements within the SVG image, helping you avoid unnecessarily repetitive markup. There are four basic elements that enable this behavior: <g>
, <use>
, <defs>
, and <symbol>
.
These elements will become particularly important in Chapter 10 when we discuss SVG sprite sheets.
Let’s say that we’ve re-created the basic shape of the Olympic rings in SVG using the following markup:
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 120 65"
>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"25"
stroke=
"rgb(11,112,191)"
cy=
"25"
r=
"15"
></circle>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"40"
stroke=
"rgb(240,183,0)"
cy=
"40"
r=
"15"
></circle>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"60"
stroke=
"rgb(0,0,0)"
cy=
"25"
r=
"15"
></circle>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"75"
stroke=
"rgb(13,146,38)"
cy=
"40"
r=
"15"
></circle>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"95"
stroke=
"rgb(214,0,23)"
cy=
"25"
r=
"15"
></circle>
</svg>
Each circle is its own separate element. We define the position of each circle and apply a colored stroke to create our rings. When viewed together they appear as the Olympic rings (see Figure 6-2). Since we really want them as a unified object in this case, we can group them together using the group element (<g>
), which is intended for logically grouping related elements together.
Here’s what our markup looks like if we group the circles together:
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 115 65"
>
<g
id=
"rings"
>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"25"
stroke=
"rgb(11,112,191)"
cy=
"25"
r=
"15"
/>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"40"
stroke=
"rgb(240,183,0)"
cy=
"40"
r=
"15"
/>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"60"
stroke=
"rgb(0,0,0)"
cy=
"25"
r=
"15"
/>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"75"
stroke=
"rgb(13,146,38)"
cy=
"40"
r=
"15"
/>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"95"
stroke=
"rgb(214,0,23)"
cy=
"25"
r=
"15"
/>
</g>
</svg>
Nothing has changed visually; we’ve just added a little more structure to our image by grouping those circles together and providing an id
to refer to the group.
Structure is nice from a maintenance perspective, but by grouping these elements, we can also simplify our markup a little. If you notice, while the position and color of each circle differ, the fill and stroke-width remain the same. Attributes that are applied to a group element also apply to descendants of the group element. We can take advantage of this and pull out the stroke-width and fill, applying them to the group instead:
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 115 65"
>
<g
id=
"rings"
stroke-width=
"3"
fill=
"none"
>
<circle
cx=
"25"
stroke=
"rgb(11,112,191)"
cy=
"25"
r=
"15"
/>
<circle
cx=
"40"
stroke=
"rgb(240,183,0)"
cy=
"40"
r=
"15"
/>
<circle
cx=
"60"
stroke=
"rgb(0,0,0)"
cy=
"25"
r=
"15"
/>
<circle
cx=
"75"
stroke=
"rgb(13,146,38)"
cy=
"40"
r=
"15"
/>
<circle
cx=
"95"
stroke=
"rgb(214,0,23)"
cy=
"25"
r=
"15"
/>
</g>
</svg>
Our markup is now both easier to read and less repetitive. Grouping also provides the benefit of making it much easier to transform these elements in CSS, or make them interactive using JavaScript. Instead of having to wrangle each individual element, we can apply our CSS or attach our JavaScript directly to the group element, and the descendants will all follow along.
Grouping these elements together also allows us to easily repeat the group by taking advantage of the <use>
element.
In order to use the xlink:href attribute without any errors, you’ll need to include the xlink namespace on the root svg element like so:
<svg
xmlns=
"http://www.w3.org/2000/svg"
xmlns:xlink=
"http://www.w3.org/1999/xlink"
>
The <use>
element allows you to refer to other content (like a group element) using the xlink:href
attribute. You can then use the x
and y
attributes to move the new group’s origin to a new location. It’s important to note that the x and y coordinates are relative to the position of the original element.
For example, if we wanted to add another set of rings below our first set, we could expand our viewBox
slightly and then reuse the group like so:
<svg
xmlns=
"http://www.w3.org/2000/svg"
xmlns:xlink=
"http://www.w3.org/1999/xlink"
viewBox=
"0 0 115 130"
>
<g
id=
"rings"
stroke-width=
"3"
fill=
"none"
>
<circle
cx=
"25"
stroke=
"rgb(11,112,191)"
cy=
"25"
r=
"15"
/>
<circle
cx=
"40"
stroke=
"rgb(240,183,0)"
cy=
"40"
r=
"15"
/>
<circle
cx=
"60"
stroke=
"rgb(0,0,0)"
cy=
"25"
r=
"15"
/>
<circle
cx=
"75"
stroke=
"rgb(13,146,38)"
cy=
"40"
r=
"15"
/>
<circle
cx=
"95"
stroke=
"rgb(214,0,23)"
cy=
"25"
r=
"15"
/>
</g>
<use
x=
"0"
y=
"65"
xlink:href=
"#rings"
/>
</svg>
In this example, we reference the rings
group using the xlink:href
attribute. We then specify that we want the starting x coordinate to be identical to the starting x coordinate of the original element (x="0"
) and that we’d like to shift the image down from the starting y coordinate of the original element (y="65"
). The result is two sets of identical rings, stacked vertically (Figure 6-3).
We could also have referenced an external SVG file inside our use
element. The following snippet shows how you would do that:
<use
x=
"0"
y=
"65"
xlink:href=
"/other/svg/file.svg#rings"
/>
Depending on what you’re trying to accomplish, referencing these files externally may be the ideal option, as it enables each SVG file you are using to be cached individually. There are browser support issues (in Internet Explorer prior to version 11) to be aware of, but we’ll discuss how to navigate those when we explore SVG spriting.
Hopefully by now it’s becoming clear how useful the group element can be. However, you may have also noticed a catch: the group element is rendered onto the canvas. In the examples we’ve been showing, that’s OK. There are times, though, where you may want to have a pattern grouped together that you don’t want to render until you’ve included it elsewhere using the <use>
element. To accomplish this, we can wrap the group
element within a <defs>
element—allowing us to define the rings as a set of grouped elements without having them rendered:
<svg
xmlns=
"http://www.w3.org/2000/svg"
xmlns:xlink=
"http://www.w3.org/1999/xlink"
viewBox=
"0 0 115 65"
>
<defs>
<g
id=
"rings"
stroke-width=
"3"
fill=
"none"
>
<circle
cx=
"25"
stroke=
"rgb(11,112,191)"
cy=
"25"
r=
"15"
/>
<circle
cx=
"40"
stroke=
"rgb(240,183,0)"
cy=
"40"
r=
"15"
/>
<circle
cx=
"60"
stroke=
"rgb(0,0,0)"
cy=
"25"
r=
"15"
/>
<circle
cx=
"75"
stroke=
"rgb(13,146,38)"
cy=
"40"
r=
"15"
/>
<circle
cx=
"95"
stroke=
"rgb(214,0,23)"
cy=
"25"
r=
"15"
/>
</g>
</defs>
<use
x=
"0"
y=
"65"
xlink:href=
"#rings"
/>
</svg>
In this example, we’ve wrapped our group within a defs
element, which stops it from being rendered. It’s defined but not displayed. This alters the way the x and y coordinates on the use
element behave. With group elements, since they are displayed, the x and y coordinates on the use
element are relative to the original group. Now that the defs
element has stopped the group from being displayed, however, there is no original element to position relative to. Instead, we’re back to the familiar process of positioning our use
element relative to the user coordinate system.
The final element related to grouping and reuse is the symbol
element. The symbol
element has some similarities both to the <g>
element and the defs
element. Like the <g>
element, it is intended as a way to logically group related elements together. Like the defs
element, however, any symbol
s that are defined are not rendered until they’re referenced later by a use
element.
The symbol
element also differs from the group element in that it can have its own viewBox
and preserveAspectRatio
attributes, letting you alter the way it fits within the viewport.
Other than that, it’s used in the same way as the <g>
element, so the markup is nearly identical:
<svg
xmlns=
"http://www.w3.org/2000/svg"
xmlns:xlink=
"http://www.w3.org/1999/xlink"
viewBox=
"0 0 115 65"
>
<symbol
id=
"rings"
>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"25"
stroke=
"rgb(11,112,191)"
cy=
"25"
r=
"15"
/>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"40"
stroke=
"rgb(240,183,0)"
cy=
"40"
r=
"15"
/>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"60"
stroke=
"rgb(0,0,0)"
cy=
"25"
r=
"15"
/>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"75"
stroke=
"rgb(13,146,38)"
cy=
"40"
r=
"15"
/>
<circle
stroke-width=
"3"
fill=
"none"
cx=
"95"
stroke=
"rgb(214,0,23)"
cy=
"25"
r=
"15"
/>
</symbol>
<use
x=
"0"
y=
"0"
xlink:href=
"#rings"
/>
</svg>
We’ll go into more detail about some of the other differences and gotchas around each of these grouping elements in Chapter 10.
If you’ve ever opened Photoshop (or something similar), you’re probably familiar with the idea of filters—graphic operations such as drop shadows and blurs that you can apply to your image. The advantage of a vector format in this regard is that the filtering can be done through markup or applied with CSS.
Filters are essentially post-processing steps for your images. For an HTML page to be loaded within a browser, the browser needs to go through a series of steps to construct the page: grab all the elements, apply the styles, lay out the elements on the screen, and finally render the page. Filtering introduces one more step in the process. Just before the page is displayed on the screen, the filter is overlaid on any images it is being applied to.
You can apply filters to your SVG images in one of two ways. The first is to use the filter
element to define a filter that can then be referred to later in the SVG file.
Here’s an example of what a drop shadow might look like when applied to our Olympic rings:
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 115 65"
>
<defs>
<filter
id=
"dropShadow"
>
<feGaussianBlur
in=
"SourceAlpha"
stdDeviation=
"1"
></feGaussianBlur>
<feOffset
dx=
"1"
dy=
"1"
result=
"offsetblur"
></feOffset>
<feFlood
flood-color=
"#000000"
></feFlood>
<feComposite
in2=
"offsetblur"
operator=
"in"
></feComposite>
<feMerge>
<feMergeNode></feMergeNode>
<feMergeNode
in=
"SourceGraphic"
></feMergeNode>
</feMerge>
</filter>
</defs>
<g
id=
"rings"
stroke-width=
"3"
fill=
"none"
filter=
"url(#dropShadow)"
>
<circle
cx=
"25"
stroke=
"rgb(11,112,191)"
cy=
"25"
r=
"15"
/>
<circle
cx=
"40"
stroke=
"rgb(240,183,0)"
cy=
"40"
r=
"15"
/>
<circle
cx=
"60"
stroke=
"rgb(0,0,0)"
cy=
"25"
r=
"15"
/>
<circle
cx=
"75"
stroke=
"rgb(13,146,38)"
cy=
"40"
r=
"15"
/>
<circle
cx=
"95"
stroke=
"rgb(214,0,23)"
cy=
"25"
r=
"15"
/>
</g>
</svg>
The only thing we changed regarding our original group of rings is that we’ve added the filter
attribute, referring to the id
of the new filter we just defined. The rest of the magic is happening inside that filter
element. Let’s walk through it.
The fourth line defines the filter and gives it an id
of "dropShadow"
so that we can refer to it later.
Lines 5 through 10 are filter primitive elements. The filter primitives are what actually describe the work to be done. The browser will walk through each primitive, applying it step by step, to create the overall filter. Digging deep into all of the native filters is a little beyond the scope of this chapter: we’re introducing this concept only to highlight a few performance gotchas. The result of each of those primitives in this case is a small drop shadow. When we apply it to our rings group, each individual circle will have a drop shadow applied.
You can achieve a similar effect by taking advantage of CSS filter effects. In fact, the whole thing becomes much simpler. Here’s a drop shadow applied to our rings using CSS (see Figure 6-4):
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 115 65"
style=
"-webkit-filter: drop-shadow(2px 5px 7px black);"
>
<g
id=
"rings"
stroke-width=
"3"
fill=
"none"
>
<circle
cx=
"25"
stroke=
"rgb(11,112,191)"
cy=
"25"
r=
"15"
></circle>
<circle
cx=
"40"
stroke=
"rgb(240,183,0)"
cy=
"40"
r=
"15"
></circle>
<circle
cx=
"60"
stroke=
"rgb(0,0,0)"
cy=
"25"
r=
"15"
></circle>
<circle
cx=
"75"
stroke=
"rgb(13,146,38)"
cy=
"40"
r=
"15"
></circle>
<circle
cx=
"95"
stroke=
"rgb(214,0,23)"
cy=
"25"
r=
"15"
></circle>
</g>
</svg>
Unfortunately, support for CSS filters is still limited at the time of writing, with most browsers that do support them having hidden them behind a prefix or configuration flag.
Because this does introduce a new step in the process of displaying a page, you want to be careful with how many filters you apply: they’re not free. Usually, if you’re careful about how many filters you apply, it’s not too much of an issue. Where it becomes a bit more serious is when you try applying any filter that involves some sort of blurring—effects such as applying a drop shadow or, well, a blur. This is because the process of creating the blur is rather involved.
Let’s say you apply a 10-pixel blur to your SVG image. In order for the browser to display it properly, it needs to walk through the original image, pixel by pixel. For each pixel of the original image, it must also look at the surrounding 10 pixels in every direction in order to properly calculate the colors to be displayed. This process becomes more unwieldy the larger the radius. The browser will try to optimize this process as much as possible, but you can still run into some rather serious performance issues—particularly on an underpowered device.
Filters are incredibly powerful and useful. Mostly, if they’re kept in check, the performance isn’t too bad, other than those filters involving some level of blurring. If you’re using a graphics editing program to apply these filters, you’ll want to pay very close attention to the outputted code: it may not be doing exactly what you think.
For example, Adobe Illustrator has a number of filters and image effects listed in the Effect menu option (see Figure 6-5). If you apply a drop shadow from here and then view the resulting code, you’ll see something like this:
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 115 65"
style=
"-webkit-filter: drop-shadow(2px 5px 7px black);"
>
<g
id=
"rings"
stroke-width=
"3"
fill=
"none"
>
<image
overflow=
"visible"
width=
"451"
height=
"212"
xlink:href=
"78ABE.png"
transform=
"matrix(1 0 0 1 37 146)"
>
</image>
<circle
cx=
"25"
stroke=
"rgb(11,112,191)"
cy=
"25"
r=
"15"
></circle>
<circle
cx=
"40"
stroke=
"rgb(240,183,0)"
cy=
"40"
r=
"15"
></circle>
<circle
cx=
"60"
stroke=
"rgb(0,0,0)"
cy=
"25"
r=
"15"
></circle>
<circle
cx=
"75"
stroke=
"rgb(13,146,38)"
cy=
"40"
r=
"15"
></circle>
<circle
cx=
"95"
stroke=
"rgb(214,0,23)"
cy=
"25"
r=
"15"
></circle>
</g>
</svg>
Instead of using the native SVG filters, the effect creates a rasterized image of the drop shadow (78ABE.png) that is then linked to and layered into the final image. In this example, that external image was 22 KB. For reference, the original example where we used the filter
element weighs in at a mere 853 bytes uncompressed. On top of the file size differences, we’ve seen that rasterized images don’t scale as efficiently. Needless to say, this output is less than ideal.
If you’re applying filters inside of Illustrator, make sure you explicity instruct Illustrator to use an SVG filter by using the Effects → SVG Filters option. From there you can handcode your filter and have that applied, avoiding the inefficiency of Illustrator’s default process (see Figure 6-6).
We’ve covered most of the basics of the SVG format. Along the way we touched on a few things for you to keep in mind in terms of performance, but it’s time now to take a deeper look at specific optimizations that you can apply to reduce the footprint and performance impact of your SVG images.
One of the advantages of the SVG format being text-based is that it compresses very well. As we’ve seen, the SVG format is an XML-based format and XML formats tend to be fairly repetitive. This plays right into the hands of compression algorithms such as GZip and the up-and-coming Brotli (see “What About Brotli?”). As a result, applying one of these compression methods to your SVG files should be the very first optimization that you consider by default. Even if you were to do nothing else, you would still see a significant savings in file size.
You can apply GZip to your SVG files ahead of time, or have your server compress the files when served. If you compress them ahead of time (which is ideal, because it means you can use the high-efficiency Zopfli compressor), it’s recommended that you use the .svgz extension to helpfully distinguish between the compressed and uncompressed SVG files you’re providing. You’ll also need to make sure that the server communicates to the browser that the image has GZip applied to it. Otherwise, the browser will not be able to display it.
If you’re using Apache, you’ll want to add the following lines to your .htaccess file to ensure the browser knows that any SVGZ images have GZip applied to them:
<IfModule mod_mime.c> AddEncoding gzip svgz </IfModule>
If you want the server to apply compression on the fly, you’ll need to configure it to do so. In Apache, that means making sure the following lines are in your .htaccess file:
<IfModule mod_filter.c> AddOutputFilterByType DEFLATE "image/svg+xml" </IfModule>
Regardless of whether you’re compressing ahead of time or on the fly, you’ll also want to ensure that any SVG and SVGZ images are served with the appropriate MIME type:
AddType image/svg+xml svg svgz
You can verify that SVG files are being served appropriately, with GZip correctly applied, by checking to make sure that any SVG images are sent with the following headers:
Content-Type: image/svg+xml Vary: Accept-Encoding
Any SVGZ files passed from the server should also carry the Content-Encoding
header:
Content-Type: image/svg+xml Content-Encoding: gzip Vary: Accept-Encoding
If you’re seeing these headers correctly returned and your images are displaying, you’re successfully reaping the rewards of GZip compression on your SVG images.
While enabling compression and minifying your SVG files will greatly reduce the file size, they don’t help in terms of reducing the complexity of the image to begin with. There are, however, a few simple optimizations you can apply that will help on that front.
Many of the optimizations you can apply to the SVG format all revolve around the same basic principle: reduce complexity. We touched on this briefly when we discussed the basic shapes defined in SVG and compared them to the path
element. Using the basic shapes wherever possible not only makes it easier for browsers during the rasterization process, but it also reduces the complexity and amount of markup necessary to produce the image in the first place.
There are several other optimizations that accomplish the same basic goal:
If you use paths within your image (and you’ll need to in order to draw anything a bit more complex), you’ll want to try to keep those paths as simple as possible. The fewer points and commands you include in a path, the less markup you’ll use and the fewer instructions the client will have to parse through and decipher. The end result will be a lighter image that displays more quickly.
If you’re using a graphics editing program (like the popular Adobe Illustrator) to produce your SVG images, you’ll likely see that many of the points defined within the various shapes and paths are incredibly precise. Rarely is that level of precision needed. In most cases, you can safely round off to one or two decimal points without any noticeable visual degradation in the image. It sounds fairly trivial, but removing those unnecessary decimal points can really add up in terms of file size.
If an SVG image has several paths of a similar shape and style, it may make sense to merge them together—reducing the number of paths necessary for the SVG image to be drawn. You’ll want to be careful with this one, though: if paths overlap, then trying to merge them can result in some serious visual degredation. This is one optimization you’ll want to do by hand.
SVG makes including actual text pretty trivial. In fact, it can be as simple as using the <text>
element, like so:
<svg
xmlns=
"http://www.w3.org/2000/svg"
viewBox=
"0 0 100 100"
>
<text
font-family=
"Times New Roman"
font-size=
"10"
x=
"0"
y=
"20"
>
Hi. I'm text.</text>
</svg>
That’s it. That’s all there is to it. We set our font styling, and type out our text, and it’s now included within our SVG image. Beyond making it easy to add text to an SVG image, the <text>
element adds a few other benefits as well:
Because the <text>
element allows you to embed your text plainly, it’s searchable and accessible.
The <text>
element relies on the font itself. Many well-designed fonts have features, like hinting, that enable them to adjust the display for different sizes, maximizing visual appeal and readability.
Unfortunately, this second feature also causes some problems. If the font is not one that is available on the system, the text will revert to the system default. Considering that this text lives in an image, and is therefore most likely specifically chosen to appear in a certain way, this is not exactly ideal.
Now it is entirely possible to use a web font to render that text, but that requires the additional request and weight associated with making that request—and when it comes to web fonts, that weight is usually not trivial.
In order to ensure that your text appears as styled, no matter the font available on the viewer’s operating system, you can elect to convert that text into outlines. This process involves the graphic editor creating an outline of the text, then creating a path that matches the shape of that outline.
This eliminates the need for the additional request and weight needed to include the web font, but it does mean that the SVG file itself becomes substantially more complex. This tradeoff is quite frequently acceptable when viewed purely from a performance standpoint: as we’ve discussed, SVG compresses incredibly well and the extra bloat within the SVG file itself is not likely to come anywhere near the size of the actual font.
You also lose a bit of the accessibility that the <text>
element provides. Because the text is no longer text, but instead a path comprising a series of points, the text becomes inaccessible and unsearchable. You can bring that back to an extent by using the <title>
element and including an appropriate textual representation of the image there.
There are a few options for SVG optimization tools, but SVG Optimizer (SVGO) stands above the rest for its large number of optimizations and the wide range of tools that take advantage of it. At its core, SVGO is a Node.js-based tool, but there is also a web app, a Grunt task, a Gulp task, and plug-ins for Sketch, Illustrator, and Inkscape. For the purpose of this section, we’ll zero in on the command-line interface provided by the core tool and the web app: SVGOMG.
If you have Node.js already installed on your machine, then you can install SVGO using the npm install
command (optionally using sudo
):
[sudo] npm install -g svgo
You can verify that it’s properly installed by entering svgo
into your command line and pressing Enter. If SVGO is ready to go, you’ll see a list of options that can be used with SVGO.
To run SVGO on an SVG image, you type the svgo
command followed by the file name:
svgo myimage.svg
Even if you run SVGO using the default configuation, you’re almost certainly going to see some performance gains in terms of file size. Just about every optimization we’ve discussed—reducing decimal points, merging and simplifying paths—can be done automatically by SVGO.
You will want to pay close attention to the defaults, however, because it’s unlikely that everything is going to be set up the way you want. While many of the default optimizations are harmless, a few are riskier to automate. I’ve seen many cases where combining shapes ended up distorting the image in some way. Other “optimizations,” such as removing the viewBox
, can actually be detrimental for reasons we’ve already discussed.
You can override the default configuration by creating your own configuration and passing that file to the SVGO command. The configuration file can be as simple as a list of the SVGO plug-ins you’d like to execute, stored in a Yet Another Markup Language (YAML) file. For example:
plugins
:
-
removeDoctype
-
removeComments
-
cleanupAttrs
-
minifyStyles
....
If we stored this in a file called myconfig.yml, we could then pass that configuration to SVGO using the --config
flag as follows:
svgo --config myconfig.yml myimage.svg
The command-line interface, as well as the Grunt and Gulp tasks built on top of it, is an excellent option for automation. If you need to dig deeper into a specific file, though, then the web app SVGOMG, built by Jake Archibald and shown in Figure 6-7, may be a better option.
SVGOMG provides all the functionality of the underlying SVGO Node.js tool, but with one notable improvement: it lets you visualize the difference both in the code and the image itself.
Using SVGOMG, you upload your SVG file and are immediately presented with it in all its visual glory. You can then select and deselect SVGO options to see what, if any, difference they make visually as well as in terms of file size.
SVGOMG also allows you to view the code of the optimized SVG image. The code is updated as each SVGO optimization is toggled on or off, allowing you to see exactly what is being changed.
When you’re done tweaking the image, you can download it back to your machine.
In this chapter we dug deeper into the differences between raster formats and vector formats. We walked through the basics of the SVG format, and talked about how you can minimize the performance impact through a series of optimizations.
You now have a really solid understanding of the various image formats and how to effectively compress them. In Part II of this book, we’ll shift our focus to how those images are actually loaded by the browser.