diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,14 @@
+## 2.9.13
+
+* Change internal attributes representation from HashMap to Map. This
+  introduces stable ordering, at a negligible performance cost for
+  realistic element sizes. This may affect some test suites.
+* doctype no longer accepts attributes. You can use `with` with
+  `doctypeHtml` now, if needed.
+
 ## 2.9.12.1
 
-* Allow different orderings of attributes in test-suite 
+* Allow different orderings of attributes in test-suite
 
 ## 2.9.12
 
diff --git a/benchmarks/HtmlBenchmarks.hs b/benchmarks/HtmlBenchmarks.hs
--- a/benchmarks/HtmlBenchmarks.hs
+++ b/benchmarks/HtmlBenchmarks.hs
@@ -7,12 +7,12 @@
 module HtmlBenchmarks where
 
 import           Data.Monoid (Monoid,mappend,mempty)
+import           Data.Text (Text)
 import qualified Data.Text as T
 -- import qualified Data.Text.Lazy.Builder as B
 
 import qualified Prelude as P
 import           Prelude hiding (div, id)
-import           Data.String
 
 -- import BenchmarkUtils
 import           Lucid
@@ -22,10 +22,10 @@
 -- | Description of an HTML benchmark
 --
 data HtmlBenchmark = forall a. HtmlBenchmark
-    String       -- ^ Name.
+    String          -- ^ Name.
     (a -> Html ())  -- ^ Rendering function.
-    a            -- ^ Data.
-    (Html ())         -- ^ Longer description.
+    a               -- ^ Data.
+    (Html ())       -- ^ Longer description.
 
 -- | List containing all benchmarks.
 --
@@ -40,15 +40,19 @@
     , HtmlBenchmark "wideTree" wideTree wideTreeData $
         "A very wide tree (" >> toHtml (show (length wideTreeData)) >> " elements)"
     , HtmlBenchmark "wideTreeEscaping" wideTree wideTreeEscapingData $ do
-        "A very wide tree (" >> toHtml (show (length wideTreeData)) >> " elements)"
+        "A very wide tree (" >> toHtml (show (length wideTreeEscapingData)) >> " elements)"
         " with lots of escaping"
     , HtmlBenchmark "deepTree" deepTree deepTreeData $ do
         "A really deep tree (" >> toHtml (show deepTreeData) >> " nested templates)"
     , HtmlBenchmark "manyAttributes" manyAttributes manyAttributesData $ do
         "A single element with " >> toHtml (show (length manyAttributesData))
-        " attributes."
+        "  distinct attributes."
+    , HtmlBenchmark "duplicateAttributes" duplicateAttributes duplicateAttributesData $ do
+        "A single element with a single attribute and " >> toHtml (show (length duplicateAttributesData))
+        " values."
     , HtmlBenchmark "customAttribute" customAttributes customAttributesData $
-        "Creating custom attributes"
+        "Creating custom attributes (middle ground between manyAttributes and duplicateAttributes)"
+
     ]
 
 rows :: Int
@@ -58,20 +62,20 @@
 bigTableData = replicate rows [1..10]
 {-# NOINLINE bigTableData #-}
 
-basicData :: (String, String, [String])
+basicData :: (Text, Text, [Text])
 basicData = ("Just a test", "joe", items)
 {-# NOINLINE basicData #-}
 
-items :: [String]
-items = map (("Number " `mappend`) . show) [1 :: Int .. 14]
+items :: [Text]
+items = map (("Number " `mappend`) . T.pack . show) [1 :: Int .. 14]
 {-# NOINLINE items #-}
 
-wideTreeData :: [String]
+wideTreeData :: [Text]
 wideTreeData = take 5000 $
     cycle ["λf.(λx.fxx)(λx.fxx)", "These old days", "Foobar", "lol", "x ∈ A"]
 {-# NOINLINE wideTreeData #-}
 
-wideTreeEscapingData :: [String]
+wideTreeEscapingData :: [Text]
 wideTreeEscapingData = take 1000 $
     cycle ["<><>", "\"lol\"", "<&>", "'>>'"]
 {-# NOINLINE wideTreeEscapingData #-}
@@ -80,10 +84,15 @@
 deepTreeData = 1000
 {-# NOINLINE deepTreeData #-}
 
-manyAttributesData :: [String]
-manyAttributesData = wideTreeData
+manyAttributesData :: [(T.Text, T.Text)]
+manyAttributesData = zipWith mk [0 ..] wideTreeData where
+    mk :: Int -> T.Text -> (T.Text, T.Text)
+    mk i val = (T.pack ("attr" ++ show i), val)
 
-customAttributesData :: [(String, String)]
+duplicateAttributesData :: [Text]
+duplicateAttributesData = wideTreeData
+
+customAttributesData :: [(Text, Text)]
 customAttributesData = zip wideTreeData wideTreeData
 
 -- | Render the argument matrix as an HTML table.
@@ -97,7 +106,7 @@
 
 -- | Render a simple HTML page with some data.
 --
-basic :: (String, String, [String])  -- ^ (Title, User, Items)
+basic :: (Text, Text, [Text])  -- ^ (Title, User, Items)
       -> Html ()                        -- ^ Result.
 basic (title', user, items') = html_ $ do
     head_ $ title_ $ toHtml title'
@@ -112,7 +121,7 @@
 
 -- | A benchmark producing a very wide but very shallow tree.
 --
-wideTree :: [String]  -- ^ Text to create a tree from.
+wideTree :: [Text]  -- ^ Text to create a tree from.
          -> Html ()      -- ^ Result.
 wideTree = div_ . mapM_ ((with p_ [id_ "foo"]) . toHtml)
 
@@ -125,11 +134,15 @@
 
 -- | Create an element with many attributes.
 --
-manyAttributes :: [String]  -- ^ List of attribute values.
+manyAttributes :: [(T.Text, T.Text)]  -- ^ List of attribute values.
                -> Html ()      -- ^ Result.
-manyAttributes as = img_ (map (id_ . T.pack) as)
+manyAttributes as = img_ (map (\(key, val) -> makeAttribute key val) as)
 
-customAttributes :: [(String, String)]  -- ^ List of attribute name, value pairs
+duplicateAttributes :: [Text]  -- ^ List of attribute values.
+               -> Html ()      -- ^ Result.
+duplicateAttributes as = img_ (map id_ as)
+
+customAttributes :: [(Text, Text)]  -- ^ List of attribute name, value pairs
                  -> Html ()                -- ^ Result
 customAttributes xs =
-  img_ (map (\(key,val) -> makeAttribute (fromString key) (T.pack val)) xs)
+  img_ (map (\(key,val) -> makeAttribute key val) xs)
diff --git a/lucid.cabal b/lucid.cabal
--- a/lucid.cabal
+++ b/lucid.cabal
@@ -1,5 +1,5 @@
 name:                lucid
-version:             2.9.12.1
+version:             2.9.13
 synopsis:            Clear to write, read and edit DSL for HTML
 description:
   Clear to write, read and edit DSL for HTML.
diff --git a/src/Lucid/Base.hs b/src/Lucid/Base.hs
--- a/src/Lucid/Base.hs
+++ b/src/Lucid/Base.hs
@@ -53,8 +53,8 @@
 import qualified Data.ByteString.Lazy as L
 import qualified Data.ByteString as S
 import           Data.Functor.Identity
-import           Data.HashMap.Strict (HashMap)
-import qualified Data.HashMap.Strict as M
+import           Data.Map.Strict (Map)
+import qualified Data.Map.Strict as M
 import           Data.Hashable (Hashable(..))
 import           Data.Semigroup (Semigroup (..))
 import           Data.Monoid (Monoid (..))
@@ -86,7 +86,7 @@
 -- | A monad transformer that generates HTML. Use the simpler 'Html'
 -- type if you don't want to transform over some other monad.
 newtype HtmlT m a =
-  HtmlT {runHtmlT :: m (HashMap Text Text -> Builder,a)
+  HtmlT {runHtmlT :: m (Map Text Text -> Builder,a)
          -- ^ This is the low-level way to run the HTML transformer,
          -- finally returning an element builder and a value. You can
          -- pass 'mempty' for this argument for a top-level call. See
@@ -355,7 +355,7 @@
       toPair (Attribute x y) = (x,y)
 
 -- | Union two sets of arguments and append duplicate keys.
-unionArgs :: HashMap Text Text -> HashMap Text Text -> HashMap Text Text
+unionArgs :: Map Text Text -> Map Text Text -> Map Text Text
 unionArgs = M.unionWith (<>)
 
 --------------------------------------------------------------------------------
@@ -529,7 +529,7 @@
      else s "=\"" <> Blaze.fromHtmlEscapedText val <> s "\""
 
 -- | Folding and monoidally appending attributes.
-foldlMapWithKey :: Monoid m => (k -> v -> m) -> HashMap k v -> m
+foldlMapWithKey :: Monoid m => (k -> v -> m) -> Map k v -> m
 foldlMapWithKey f = M.foldlWithKey' (\m k v -> m `mappend` f k v) mempty
 
 -- | Convenience function for constructing builders.
diff --git a/src/Lucid/Html5.hs b/src/Lucid/Html5.hs
--- a/src/Lucid/Html5.hs
+++ b/src/Lucid/Html5.hs
@@ -15,8 +15,12 @@
 -- Elements
 
 -- | @DOCTYPE@ element
+--
+-- This is implemented as "raw output", because the doctype doesn't
+-- accept attributes, such as those inserted via 'with'.
+--
 doctype_ :: Applicative m => HtmlT m ()
-doctype_ = makeElementNoEnd "!DOCTYPE HTML"
+doctype_ = HtmlT (pure (const "<!DOCTYPE HTML>", ()))
 
 -- | @DOCTYPE@ element + @html@ element
 doctypehtml_ :: Applicative m => HtmlT m a -> HtmlT m a
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -166,7 +166,7 @@
                  [class_ "foo",style_ "attrib"]
                  (style_ "")) ==?*
         [ "<p style=\"attrib\" class=\"foo\"><style></style></p>"
-        , "<pclass=\"foo\" style=\"attrib\"><style></style></p>"
+        , "<p class=\"foo\" style=\"attrib\"><style></style></p>"
         ]
      it "no closing" $
         renderText (with p_ [class_ "foo"] (with (input_ [type_ "text"]) [class_ "zot"])) ==?*
