diff --git a/src/vte.cc b/src/vte.cc
index 82661faa..ccf00dc3 100644
--- a/src/vte.cc
+++ b/src/vte.cc
@@ -8121,7 +8121,7 @@ VteTerminalPrivate::VteTerminalPrivate(VteTerminal *t) :
         m_double_underline_position = 1;
         m_double_underline_thickness = 1;
         m_undercurl_position = 1.;
-        m_undercurl_thickness = 1;
+        m_undercurl_thickness = 1.;
 	m_strikethrough_position = 1;
         m_strikethrough_thickness = 1;
         m_overline_position = 1;
@@ -9023,14 +9023,12 @@ VteTerminalPrivate::draw_cells(struct _vte_draw_text_request *items,
                                                     &dc, VTE_DRAW_OPAQUE);
                                 break;
                         case 3:
-                                for (int j = 0; j < columns; j++) {
-                                        _vte_draw_draw_undercurl(m_draw,
-                                                                 x + j * column_width,
-                                                                 y + m_undercurl_position,
-                                                                 column_width,
-                                                                 m_undercurl_thickness,
-                                                                 &dc, VTE_DRAW_OPAQUE);
-                                }
+                                _vte_draw_draw_undercurl(m_draw,
+                                                         x,
+                                                         y + m_undercurl_position,
+                                                         m_undercurl_thickness,
+                                                         columns,
+                                                         &dc, VTE_DRAW_OPAQUE);
                                 break;
 			}
 			if (strikethrough) {
diff --git a/src/vtedraw.cc b/src/vtedraw.cc
index 9e47bfa8..28251461 100644
--- a/src/vtedraw.cc
+++ b/src/vtedraw.cc
@@ -758,6 +758,9 @@ struct _vte_draw {
         GtkBorder char_spacing;
 
 	cairo_t *cr;
+
+        /* Cache the undercurl's rendered look. */
+        cairo_surface_t *undercurl_surface;
 };
 
 struct _vte_draw *
@@ -788,6 +791,11 @@ _vte_draw_free (struct _vte_draw *draw)
 		}
 	}
 
+        if (draw->undercurl_surface != NULL) {
+                cairo_surface_destroy (draw->undercurl_surface);
+                draw->undercurl_surface = NULL;
+        }
+
 	g_slice_free (struct _vte_draw, draw);
 }
 
@@ -907,6 +915,12 @@ _vte_draw_set_text_font (struct _vte_draw *draw,
         draw->cell_height = draw->fonts[VTE_DRAW_NORMAL]->height * cell_height_scale;
         draw->char_spacing.top = (draw->cell_height - draw->fonts[VTE_DRAW_NORMAL]->height + 1) / 2;
         draw->char_spacing.bottom = (draw->cell_height - draw->fonts[VTE_DRAW_NORMAL]->height) / 2;
+
+        /* Drop the undercurl's cached look. Will recache on demand. */
+        if (draw->undercurl_surface != NULL) {
+                cairo_surface_destroy (draw->undercurl_surface);
+                draw->undercurl_surface = NULL;
+        }
 }
 
 void
@@ -1670,36 +1684,71 @@ _vte_draw_get_undercurl_arc_height(gint width)
 }
 
 double
-_vte_draw_get_undercurl_height(gint width, int line_width)
+_vte_draw_get_undercurl_height(gint width, double line_width)
 {
         return 2. * _vte_draw_get_undercurl_arc_height(width) + line_width;
 }
 
 void
 _vte_draw_draw_undercurl(struct _vte_draw *draw,
-                         gint x, double y, gint width,
-                         int line_width,
+                         gint x, double y,
+                         double line_width,
+                         gint count,
                          vte::color::rgb const *color, double alpha)
 {
-        double rad = _vte_draw_get_undercurl_rad(width);
-        double yc = y + _vte_draw_get_undercurl_height(width, line_width) / 2.;
+        /* The end of the curly line slightly overflows to the next cell, so the canvas
+         * caching the rendered look has to be wider not to chop this off. */
+        gint x_padding = line_width + 1;  /* ceil, kind of */
+
+        gint surface_top = y;  /* floor */
 
         g_assert(draw->cr);
 
         _vte_debug_print (VTE_DEBUG_DRAW,
-                        "draw_undercurl (%d, %f, %d, color=(%d,%d,%d,%.3f))\n",
-                        x,y,width,
+                        "draw_undercurl (x=%d, y=%f, count=%d, color=(%d,%d,%d,%.3f))\n",
+                        x, y, count,
                         color->red, color->green, color->blue,
                         alpha);
 
+        if (G_UNLIKELY (draw->undercurl_surface == NULL)) {
+                /* Cache the undercurl's look. The design assumes that until the cached look is
+                 * invalidated (the font is changed), this method is always called with the "y"
+                 * parameter having the same fractional part, and the same "line_width" parameter.
+                 * For caching, only the fractional part of "y" is used. */
+                cairo_t *undercurl_cr;
+
+                double rad = _vte_draw_get_undercurl_rad(draw->cell_width);
+                double y_bottom = y + _vte_draw_get_undercurl_height(draw->cell_width, line_width);
+                double y_center = (y + y_bottom) / 2.;
+                gint surface_bottom = y_bottom + 1;  /* ceil, kind of */
+
+                _vte_debug_print (VTE_DEBUG_DRAW,
+                                  "caching undercurl shape\n");
+
+                /* Add a line_width of margin horizontally on both sides, for nice antialias overflowing. */
+                draw->undercurl_surface = cairo_surface_create_similar (cairo_get_target (draw->cr),
+                                                                        CAIRO_CONTENT_ALPHA,
+                                                                        draw->cell_width + 2 * x_padding,
+                                                                        surface_bottom - surface_top);
+                undercurl_cr = cairo_create (draw->undercurl_surface);
+                cairo_set_operator (undercurl_cr, CAIRO_OPERATOR_OVER);
+                /* First quarter circle, similar to the left half of the tilde symbol. */
+                cairo_arc (undercurl_cr, x_padding + draw->cell_width / 4., y_center - surface_top + draw->cell_width / 4., rad, M_PI * 5 / 4, M_PI * 7 / 4);
+                /* Second quarter circle, similar to the right half of the tilde symbol. */
+                cairo_arc_negative (undercurl_cr, x_padding + draw->cell_width * 3 / 4., y_center - surface_top - draw->cell_width / 4., rad, M_PI * 3 / 4, M_PI / 4);
+                cairo_set_line_width (undercurl_cr, line_width);
+                cairo_stroke (undercurl_cr);
+                cairo_destroy (undercurl_cr);
+        }
+
+        /* Paint the cached look of the undercurl using the desired look.
+         * The cached look takes the fractional part of "y" into account,
+         * here we only offset by its integer part. */
+        cairo_save (draw->cr);
         cairo_set_operator (draw->cr, CAIRO_OPERATOR_OVER);
-        cairo_new_sub_path(draw->cr);
-        /* First quarter circle, similar to the left half of the tilde symbol. */
-        cairo_arc (draw->cr, x + width / 4., yc + width / 4., rad, M_PI * 5 / 4, M_PI * 7 / 4);
-        /* Second quarter circle, similar to the right half of the tilde symbol. */
-        cairo_arc_negative (draw->cr, x + width * 3 / 4., yc - width / 4., rad, M_PI * 3 / 4, M_PI / 4);
         _vte_draw_set_source_color_alpha (draw, color, alpha);
-        cairo_set_line_width (draw->cr, line_width);
-        /* FIXME: This is quite slow. The rendered bitmap should be cached and reused. */
-        cairo_stroke (draw->cr);
+        for (int i = 0; i < count; i++) {
+                cairo_mask_surface (draw->cr, draw->undercurl_surface, x - x_padding + i * draw->cell_width, surface_top);
+        }
+        cairo_restore (draw->cr);
 }
diff --git a/src/vtedraw.hh b/src/vtedraw.hh
index 745b8413..b5c3d36c 100644
--- a/src/vtedraw.hh
+++ b/src/vtedraw.hh
@@ -94,12 +94,13 @@ void _vte_draw_draw_line(struct _vte_draw *draw,
                          vte::color::rgb const *color, double alpha);
 
 double
-_vte_draw_get_undercurl_height(gint width, int line_width);
+_vte_draw_get_undercurl_height(gint width, double line_width);
 
 void
 _vte_draw_draw_undercurl(struct _vte_draw *draw,
-                         gint x, double y, gint width,
-                         int line_width,
+                         gint x, double y,
+                         double line_width,
+                         gint count,
                          vte::color::rgb const *color, double alpha);
 
 G_END_DECLS
diff --git a/src/vteinternal.hh b/src/vteinternal.hh
index 2b9e7ba2..446204c3 100644
--- a/src/vteinternal.hh
+++ b/src/vteinternal.hh
@@ -655,7 +655,7 @@ public:
         long m_double_underline_position;
         long m_double_underline_thickness;
         double m_undercurl_position;
-        long m_undercurl_thickness;
+        double m_undercurl_thickness;
         long m_strikethrough_position;
         long m_strikethrough_thickness;
         long m_overline_position;
