diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,8 @@
 # Changelog
 
+- 0.1.1 (2025-02-28)
+  * Roughly 2x faster decoding performance.
+
 - 0.1.0 (2025-01-17)
   * Initial release, supporting basic encoding/decoding.
 
diff --git a/lib/Data/ByteString/Base16.hs b/lib/Data/ByteString/Base16.hs
--- a/lib/Data/ByteString/Base16.hs
+++ b/lib/Data/ByteString/Base16.hs
@@ -62,22 +62,31 @@
     -- writing as few words as possible requires performing some length
     -- checks up front
     loop
-      | l `rem` 4 == 0 = go64 bs
+      | l `rem` 4 == 0 =
+               go64 bs
       | (l - 3) `rem` 4 == 0 = case BS.splitAt (l - 3) bs of
           (chunk, etc) ->
                go64 chunk
             <> go32 (BU.unsafeTake 2 etc)
             <> go16 (BU.unsafeDrop 2 etc)
       | (l - 2) `rem` 4 == 0 = case BS.splitAt (l - 2) bs of
-          (chunk, etc) -> go64 chunk <> go32 etc
+          (chunk, etc) ->
+               go64 chunk
+            <> go32 etc
       | (l - 1) `rem` 4 == 0 = case BS.splitAt (l - 1) bs of
-          (chunk, etc) -> go64 chunk <> go16 etc
+          (chunk, etc) ->
+               go64 chunk
+            <> go16 etc
 
-      | l `rem` 2 == 0 = go32 bs
+      | l `rem` 2 == 0 =
+               go32 bs
       | (l - 1) `rem` 2 == 0 = case BS.splitAt (l - 1) bs of
-          (chunk, etc) -> go32 chunk <> go16 etc
+          (chunk, etc) ->
+               go32 chunk
+            <> go16 etc
 
-      | otherwise = go16 bs
+      | otherwise =
+               go16 bs
 
     go64 b = case BS.splitAt 4 b of
       (chunk, etc)
@@ -113,8 +122,12 @@
         let !w16 = expand_w8 h
         in  BSB.word16BE w16 <> go16 t
 
+-- word8 hex character to word4
 word4 :: Word8 -> Maybe Word8
-word4 w8 = fmap fi (BS.elemIndex w8 hex_charset)
+word4 c
+  | c > 47 && c < 58  = pure $! c - 48
+  | c > 96 && c < 103 = pure $! c - 87
+  | otherwise         = Nothing
 {-# INLINE word4 #-}
 
 -- | Decode a base16 'ByteString' to base256.
@@ -134,7 +147,8 @@
   where
     -- same story, but we need more checks
     loop
-      | l `rem` 16 == 0       = go64 mempty bs
+      | l `rem` 16 == 0 =
+            go64 mempty bs
       | (l - 2) `rem` 16 == 0 = case BS.splitAt (l - 2) bs of
           (chunk, etc) -> do
             b0 <- go64 mempty chunk
@@ -169,7 +183,8 @@
             b2 <- go16 b1 (BU.unsafeTake 4 (BU.unsafeDrop 8 etc))
             go8 b2 (BU.unsafeDrop 12 etc)
 
-      | l `rem` 8 == 0       = go32 mempty bs
+      | l `rem` 8 == 0 =
+            go32 mempty bs
       | (l - 2) `rem` 8 == 0 = case BS.splitAt (l - 2) bs of
           (chunk, etc) -> do
             b0 <- go32 mempty chunk
@@ -184,13 +199,15 @@
             b1 <- go16 b0 (BU.unsafeTake 4 etc)
             go8 b1  (BU.unsafeDrop 4 etc)
 
-      | l `rem` 4 == 0       = go16 mempty bs
+      | l `rem` 4 == 0 =
+            go16 mempty bs
       | (l - 2) `rem` 4 == 0 = case BS.splitAt (l - 2) bs of
           (chunk, etc) -> do
             b0 <- go16 mempty chunk
             go8 b0 etc
 
-      | otherwise = go8 mempty bs
+      | otherwise =
+            go8 mempty bs
 
     go64 acc b = case BS.splitAt 16 b of
       (chunk, etc)
diff --git a/ppad-base16.cabal b/ppad-base16.cabal
--- a/ppad-base16.cabal
+++ b/ppad-base16.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.0
 name:               ppad-base16
-version:            0.1.0
+version:            0.1.1
 synopsis:           Pure base16 encoding and decoding on bytestrings.
 license:            MIT
 license-file:       LICENSE
