diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+## Modern URI 0.0.2.0
+
+* Added the `renderStr` and `renderStr'` functions for efficient rendering
+  to `String` and `ShowS`.
+
+* Added the `parserBs` that can consume strict `ByteString` streams.
+
 ## Modern URI 0.0.1.0
 
 * Initial release.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -13,19 +13,20 @@
 
 ## Motivation
 
-There are already at least two libraries for working with URIs:
-[`uri`](https://hackage.haskell.org/package/uri) and
+There are already at least three libraries for working with URIs:
+[`uri`](https://hackage.haskell.org/package/uri),
+[`network-uri`](https://hackage.haskell.org/package/network-uri), and
 [`uri-bytestring`](https://hackage.haskell.org/package/uri-bytestring). Why
 write one more?
 
-Let's see first about the `uri` package:
+Let's see first about the `uri` and `network-uri` packages (they are quite
+similar):
 
-* It uses `String` instead of `Text` or `ByteString`, it is thus
-  inefficient.
+* They use `String` instead of `Text` or `ByteString`, this is inefficient.
 * The types are not very precise. Query string is represented as `Maybe
   String` for example.
-* Uses Parsec under the hood, however does not allow us to use its URI
-  parser in a bigger Parsec parser.
+* The packages use Parsec under the hood, however they do not allow us to
+  use its URI parser in a bigger Parsec parser.
 
 Now what about `uri-bytestring`?
 
@@ -51,21 +52,27 @@
   guaranteeing that every sub-component of the `URI` record is by itself
   cannot be invalid. This boils down to careful use of types and a set of
   smart constructors for things like scheme, host, etc.
+* Textual components in the `URI` data type represented as `Text` rather
+  than `ByteString`, because they are percent-decoded and so they can
+  contain characters outside of ASCII range (i.e. Unicode). This allows for
+  easier manipulation of `URI`s, while encoding and decoding headaches are
+  handled by the parsers and renders for you.
 * Absolute and relative URIs differ only by the scheme component: if it's
   `Nothing`, then URI is relative, otherwise it's absolute.
 * Megaparsec parser that can be used as a standalone smart constructor for
   the `URI` data type (see `mkURI`) as well as be seamlessly integrated into
-  a bigger Megaparsec parser.
+  a bigger Megaparsec parser that consumes strict `Text` (see `parser`) or
+  strict `ByteString` (see `parserBs`).
+* The parser performs some normalization, for example it collapses
+  consecutive slashes. Some smart constructors such as `mkScheme` and
+  `mkHost` also perform normalization. So in a sense URIs are also
+  “normalized by construction” to some extent.
 * Fast rendering to strict `Text` and `ByteString` as well as to their
-  respective `Builder` types.
+  respective `Builder` types and to `String`/`ShowS`.
 * Extensive set of lensy helpers for easier manipulation of the nested data
   types (see `Text.URI.Lens`).
 * Quasi-quoters for compile-time construction of the `URI` data type and
   refined text types (see `Text.URI.QQ`).
-
-TODO:
-
-* Provide parser that can parse URIs from `ByteString`s.
 
 ## Contribution
 
diff --git a/Text/URI.hs b/Text/URI.hs
--- a/Text/URI.hs
+++ b/Text/URI.hs
@@ -45,14 +45,18 @@
   , RTextException (..)
     -- * Parsing
   , parser
+  , parserBs
     -- * Rendering
   , render
   , render'
   , renderBs
-  , renderBs' )
+  , renderBs'
+  , renderStr
+  , renderStr' )
 where
 
-import Text.URI.Parser
+import Text.URI.Parser.ByteString
+import Text.URI.Parser.Text
 import Text.URI.Render
 import Text.URI.Types
 
diff --git a/Text/URI/Parser.hs b/Text/URI/Parser.hs
deleted file mode 100644
--- a/Text/URI/Parser.hs
+++ /dev/null
@@ -1,203 +0,0 @@
--- |
--- Module      :  Text.URI.Parser
--- Copyright   :  © 2017 Mark Karpov
--- License     :  BSD 3 clause
---
--- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
--- Stability   :  experimental
--- Portability :  portable
---
--- URI parsers, an internal module.
-
-{-# LANGUAGE DataKinds          #-}
-{-# LANGUAGE DeriveDataTypeable #-}
-{-# LANGUAGE DeriveGeneric      #-}
-{-# LANGUAGE FlexibleContexts   #-}
-{-# LANGUAGE OverloadedStrings  #-}
-{-# LANGUAGE RankNTypes         #-}
-{-# LANGUAGE RecordWildCards    #-}
-
-module Text.URI.Parser
-  ( mkURI
-  , parser
-  , ParseException (..) )
-where
-
-import Control.Applicative
-import Control.Monad
-import Control.Monad.Catch (Exception (..), MonadThrow (..))
-import Data.Char
-import Data.Data (Data)
-import Data.Maybe (isNothing, catMaybes)
-import Data.Text (Text)
-import Data.Typeable (Typeable)
-import Data.Void
-import GHC.Generics
-import Text.Megaparsec
-import Text.Megaparsec.Char
-import Text.URI.Types
-import qualified Data.ByteString.Char8      as B8
-import qualified Data.List.NonEmpty         as NE
-import qualified Data.Set                   as E
-import qualified Data.Text.Encoding         as TE
-import qualified Text.Megaparsec.Char.Lexer as L
-
--- | Construct a 'URI' from 'Text'. In case of failure 'ParseException' is
--- thrown.
---
--- This function uses the 'parser' parser under the hood, which you can also
--- use directly in a Megaparsec parser.
-
-mkURI :: MonadThrow m => Text -> m URI
-mkURI input =
-  case runParser (parser <* eof :: Parsec Void Text URI) "" input of
-    Left err -> throwM (ParseException input err)
-    Right x  -> return x
-
--- | Parse exception thrown by 'mkURI' when a given 'Text' value cannot be
--- parsed as a 'URI'.
-
-data ParseException = ParseException Text (ParseError Char Void)
-  -- ^ Arguments are: original input and parse error
-  deriving (Show, Eq, Data, Typeable, Generic)
-
-instance Exception ParseException where
-  displayException (ParseException s e) = parseErrorPretty' s e
-
--- | This parser can be used to parse 'URI' from strict 'Text'.
-
-parser :: MonadParsec e Text m => m URI
-parser = do
-  uriScheme    <- optional (try pScheme)
-  uriAuthority <- optional pAuthority
-  uriPath      <- pPath (isNothing uriAuthority)
-  uriQuery     <- option [] pQuery
-  uriFragment  <- optional pFragment
-  return URI {..}
-{-# INLINEABLE parser #-}
-
-pScheme :: MonadParsec e Text m => m (RText 'Scheme)
-pScheme = do
-  x  <- asciiAlphaChar
-  xs <- many (asciiAlphaNumChar <|> char '+' <|> char '-' <|> char '.')
-  void (char ':')
-  liftR mkScheme (x:xs)
-{-# INLINE pScheme #-}
-
-pAuthority :: MonadParsec e Text m => m Authority
-pAuthority = do
-  void (string "//")
-  authUserInfo <- optional pUserInfo
-  authHost <- pHost True >>= liftR mkHost
-  authPort <- optional (char ':' *> L.decimal)
-  return Authority {..}
-{-# INLINE pAuthority #-}
-
-pUserInfo :: MonadParsec e Text m => m UserInfo
-pUserInfo = try $ do
-  uiUsername <- label "username" $
-    many (unreservedChar <|> percentEncChar <|> subDelimChar)
-      >>= liftR mkUsername
-  uiPassword <- optional $ do
-    void (char ':')
-    many (unreservedChar <|> percentEncChar <|> subDelimChar <|> char ':')
-      >>= liftR mkPassword
-  void (char '@')
-  return UserInfo {..}
-{-# INLINE pUserInfo #-}
-
-pPath :: MonadParsec e Text m => Bool -> m [RText 'PathPiece]
-pPath hadNoAuth = do
-  doubleSlash <- lookAhead (True <$ string "//" <|> pure False)
-  when (doubleSlash && hadNoAuth) $
-    (unexpected . Tokens . NE.fromList) "//"
-  path <- flip sepBy (char '/') . label "path piece" $
-    many pchar
-  mapM (liftR mkPathPiece) (filter (not . null) path)
-{-# INLINE pPath #-}
-
-pQuery :: MonadParsec e Text m => m [QueryParam]
-pQuery = do
-  void (char '?')
-  fmap catMaybes . flip sepBy (char '&') . label "query parameter" $ do
-    let p = many (pchar' <|> char '/' <|> char '?')
-    k' <- p
-    mv <- optional (char '=' *> p)
-    k  <- liftR mkQueryKey k'
-    if null k'
-      then return Nothing
-      else Just <$> case mv of
-             Nothing -> return (QueryFlag k)
-             Just v  -> QueryParam k <$> liftR mkQueryValue v
-{-# INLINE pQuery #-}
-
-pFragment :: MonadParsec e Text m => m (RText 'Fragment)
-pFragment = do
-  void (char '#')
-  xs <- many . label "fragment character" $
-    pchar <|> char '/' <|> char '?'
-  liftR mkFragment xs
-{-# INLINE pFragment #-}
-
-----------------------------------------------------------------------------
--- Helpers
-
-liftR :: MonadParsec e s m
-  => (forall n. MonadThrow n => Text -> n r)
-  -> String
-  -> m r
-liftR f = maybe empty return . f . TE.decodeUtf8 . B8.pack
-{-# INLINE liftR #-}
-
-asciiAlphaChar :: MonadParsec e Text m => m Char
-asciiAlphaChar = satisfy isAsciiAlpha <?> "ASCII alpha character"
-{-# INLINE asciiAlphaChar #-}
-
-asciiAlphaNumChar :: MonadParsec e Text m => m Char
-asciiAlphaNumChar = satisfy isAsciiAlphaNum <?> "ASCII alpha-numeric character"
-{-# INLINE asciiAlphaNumChar #-}
-
-unreservedChar :: MonadParsec e Text m => m Char
-unreservedChar = label "unreserved character" . satisfy $ \x ->
-  isAsciiAlphaNum x || x == '-' || x == '.' || x == '_' || x == '~'
-{-# INLINE unreservedChar #-}
-
-percentEncChar :: MonadParsec e Text m => m Char
-percentEncChar = do
-  void (char '%')
-  h <- digitToInt <$> hexDigitChar
-  l <- digitToInt <$> hexDigitChar
-  return . chr $ h * 16 + l
-{-# INLINE percentEncChar #-}
-
-subDelimChar :: MonadParsec e Text m => m Char
-subDelimChar = oneOf s <?> "sub-delimiter"
-  where
-    s = E.fromList "!$&'()*+,;="
-{-# INLINE subDelimChar #-}
-
-pchar :: MonadParsec e Text m => m Char
-pchar = choice
-  [ unreservedChar
-  , percentEncChar
-  , subDelimChar
-  , char ':'
-  , char '@' ]
-{-# INLINE pchar #-}
-
-pchar' :: MonadParsec e Text m => m Char
-pchar' = choice
-  [ unreservedChar
-  , percentEncChar
-  , oneOf s <?> "sub-delimiter"
-  , char ':'
-  , char '@' ]
-  where
-    s = E.fromList "!$'()*+,;"
-{-# INLINE pchar' #-}
-
-isAsciiAlpha :: Char -> Bool
-isAsciiAlpha x = isAscii x && isAlpha x
-
-isAsciiAlphaNum :: Char -> Bool
-isAsciiAlphaNum x = isAscii x && isAlphaNum x
diff --git a/Text/URI/Parser/ByteString.hs b/Text/URI/Parser/ByteString.hs
new file mode 100644
--- /dev/null
+++ b/Text/URI/Parser/ByteString.hs
@@ -0,0 +1,248 @@
+-- |
+-- Module      :  Text.URI.Parser.ByteString
+-- Copyright   :  © 2017 Mark Karpov
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- URI parsers for string 'ByteString', an internal module.
+
+{-# LANGUAGE DataKinds          #-}
+{-# LANGUAGE FlexibleContexts   #-}
+{-# LANGUAGE OverloadedStrings  #-}
+{-# LANGUAGE RankNTypes         #-}
+{-# LANGUAGE RecordWildCards    #-}
+
+module Text.URI.Parser.ByteString
+  ( parserBs )
+where
+
+import Control.Applicative
+import Control.Monad
+import Control.Monad.Catch (MonadThrow (..))
+import Data.ByteString (ByteString)
+import Data.Char
+import Data.List (intercalate)
+import Data.Maybe (isNothing, catMaybes, maybeToList)
+import Data.Text (Text)
+import Data.Word (Word8)
+import Text.Megaparsec
+import Text.Megaparsec.Byte
+import Text.URI.Types hiding (pHost)
+import qualified Data.ByteString            as B
+import qualified Data.List.NonEmpty         as NE
+import qualified Data.Set                   as E
+import qualified Data.Text.Encoding         as TE
+import qualified Text.Megaparsec.Byte.Lexer as L
+
+-- | This parser can be used to parse 'URI' from strict 'ByteString'.
+-- Remember to use a concrete non-polymorphic parser type for efficiency.
+--
+-- @since 0.0.2.0
+
+parserBs :: MonadParsec e ByteString m => m URI
+parserBs = do
+  uriScheme    <- optional (try pScheme)
+  uriAuthority <- optional pAuthority
+  uriPath      <- pPath (isNothing uriAuthority)
+  uriQuery     <- option [] pQuery
+  uriFragment  <- optional pFragment
+  return URI {..}
+{-# INLINEABLE parserBs #-}
+
+pScheme :: MonadParsec e ByteString m => m (RText 'Scheme)
+pScheme = do
+  x  <- asciiAlphaChar
+  xs <- many (asciiAlphaNumChar <|> char 43 <|> char 45 <|> char 46)
+  void (char 58)
+  liftR mkScheme (x:xs)
+{-# INLINE pScheme #-}
+
+pAuthority :: MonadParsec e ByteString m => m Authority
+pAuthority = do
+  void (string "//")
+  authUserInfo <- optional pUserInfo
+  authHost <- pHost >>= liftR mkHost
+  authPort <- optional (char 58 *> L.decimal)
+  return Authority {..}
+{-# INLINE pAuthority #-}
+
+-- | Parser that can parse host names.
+
+pHost :: MonadParsec e ByteString m => m [Word8]
+pHost = choice
+  [ try (asConsumed ipLiteral)
+  , try (asConsumed ipv4Address)
+  , regName ]
+  where
+    asConsumed :: MonadParsec e ByteString m => m a -> m [Word8]
+    asConsumed p = B.unpack . fst <$> match p
+    ipLiteral = between (char 91) (char 93) $
+      try ipv6Address <|> ipvFuture
+    octet = do
+      pos       <- getNextTokenPosition
+      (toks, x) <- match L.decimal
+      when (x >= (256 :: Integer)) $ do
+        mapM_ setPosition pos
+        failure
+          (fmap Tokens . NE.nonEmpty . B.unpack $ toks)
+          (E.singleton . Label . NE.fromList $ "decimal number from 0 to 255")
+    ipv4Address =
+      count 3 (octet <* char 46) *> octet
+    ipv6Address = do
+      pos        <- getNextTokenPosition
+      (toks, xs) <- match $ do
+        xs' <- maybeToList <$> optional ([] <$ string "::")
+        xs  <- flip sepBy1 (char 58) $ do
+          (skip, hasMore) <- lookAhead . hidden $ do
+            skip    <- option False (True <$ char 58)
+            hasMore <- option False (True <$ hexDigitChar)
+            return (skip, hasMore)
+          case (skip, hasMore) of
+            (True,  True)  -> return []
+            (True,  False) -> [] <$ char 58
+            (False, _)     -> count' 1 4 hexDigitChar
+        return (xs' ++ xs)
+      let nskips  = length (filter null xs)
+          npieces = length xs
+      unless (nskips < 2 && (npieces == 8 || (nskips == 1 && npieces < 8))) $ do
+        mapM_ setPosition pos
+        failure
+          (fmap Tokens . NE.nonEmpty . B.unpack $ toks)
+          (E.singleton . Label . NE.fromList $ "valid IPv6 address")
+    ipvFuture = do
+      void (char 118)
+      void hexDigitChar
+      void (char 46)
+      skipSome (unreservedChar <|> subDelimChar <|> char 58)
+    regName = fmap (intercalate [46]) . flip sepBy1 (char 46) $ do
+      let ch = percentEncChar <|> asciiAlphaNumChar
+      x <- ch
+      let r = ch <|> try
+            (char 45 <* (lookAhead . try) (ch <|> char 45))
+      xs <- many r
+      return (x:xs)
+
+pUserInfo :: MonadParsec e ByteString m => m UserInfo
+pUserInfo = try $ do
+  uiUsername <- label "username" $
+    many (unreservedChar <|> percentEncChar <|> subDelimChar)
+      >>= liftR mkUsername
+  uiPassword <- optional $ do
+    void (char 58)
+    many (unreservedChar <|> percentEncChar <|> subDelimChar <|> char 58)
+      >>= liftR mkPassword
+  void (char 64)
+  return UserInfo {..}
+{-# INLINE pUserInfo #-}
+
+pPath :: MonadParsec e ByteString m => Bool -> m [RText 'PathPiece]
+pPath hadNoAuth = do
+  doubleSlash <- lookAhead (True <$ string "//" <|> pure False)
+  when (doubleSlash && hadNoAuth) $
+    (unexpected . Tokens . NE.fromList) [47,47]
+  path <- flip sepBy (char 47) . label "path piece" $
+    many pchar
+  mapM (liftR mkPathPiece) (filter (not . null) path)
+{-# INLINE pPath #-}
+
+pQuery :: MonadParsec e ByteString m => m [QueryParam]
+pQuery = do
+  void (char 63)
+  fmap catMaybes . flip sepBy (char 38) . label "query parameter" $ do
+    let p = many (pchar' <|> char 47 <|> char 63)
+    k' <- p
+    mv <- optional (char 61 *> p)
+    k  <- liftR mkQueryKey k'
+    if null k'
+      then return Nothing
+      else Just <$> case mv of
+             Nothing -> return (QueryFlag k)
+             Just v  -> QueryParam k <$> liftR mkQueryValue v
+{-# INLINE pQuery #-}
+
+pFragment :: MonadParsec e ByteString m => m (RText 'Fragment)
+pFragment = do
+  void (char 35)
+  xs <- many . label "fragment character" $
+    pchar <|> char 47 <|> char 63
+  liftR mkFragment xs
+{-# INLINE pFragment #-}
+
+----------------------------------------------------------------------------
+-- Helpers
+
+liftR :: MonadParsec e s m
+  => (forall n. MonadThrow n => Text -> n r)
+  -> [Word8]
+  -> m r
+liftR f = maybe empty return . f . TE.decodeUtf8 . B.pack
+{-# INLINE liftR #-}
+
+asciiAlphaChar :: MonadParsec e ByteString m => m Word8
+asciiAlphaChar = satisfy isAsciiAlpha <?> "ASCII alpha character"
+{-# INLINE asciiAlphaChar #-}
+
+asciiAlphaNumChar :: MonadParsec e ByteString m => m Word8
+asciiAlphaNumChar = satisfy isAsciiAlphaNum <?> "ASCII alpha-numeric character"
+{-# INLINE asciiAlphaNumChar #-}
+
+unreservedChar :: MonadParsec e ByteString m => m Word8
+unreservedChar = label "unreserved character" . satisfy $ \x ->
+  isAsciiAlphaNum x || x == 45 || x == 46 || x == 95 || x == 126
+{-# INLINE unreservedChar #-}
+
+percentEncChar :: MonadParsec e ByteString m => m Word8
+percentEncChar = do
+  void (char 37)
+  h <- restoreDigit <$> hexDigitChar
+  l <- restoreDigit <$> hexDigitChar
+  return (h * 16 + l)
+{-# INLINE percentEncChar #-}
+
+subDelimChar :: MonadParsec e ByteString m => m Word8
+subDelimChar = oneOf s <?> "sub-delimiter"
+  where
+    s = E.fromList (fromIntegral . ord <$> "!$&'()*+,;=")
+{-# INLINE subDelimChar #-}
+
+pchar :: MonadParsec e ByteString m => m Word8
+pchar = choice
+  [ unreservedChar
+  , percentEncChar
+  , subDelimChar
+  , char 58
+  , char 64 ]
+{-# INLINE pchar #-}
+
+pchar' :: MonadParsec e ByteString m => m Word8
+pchar' = choice
+  [ unreservedChar
+  , percentEncChar
+  , oneOf s <?> "sub-delimiter"
+  , char 58
+  , char 64 ]
+  where
+    s = E.fromList (fromIntegral . ord <$> "!$'()*+,;")
+{-# INLINE pchar' #-}
+
+isAsciiAlpha :: Word8 -> Bool
+isAsciiAlpha x
+  | 65 <= x && x <= 90  = True
+  | 97 <= x && x <= 122 = True
+  | otherwise           = False
+
+isAsciiAlphaNum :: Word8 -> Bool
+isAsciiAlphaNum x
+  | isAsciiAlpha x     = True
+  | 48 <= x && x <= 57 = True
+  | otherwise          = False
+
+restoreDigit :: Word8 -> Word8
+restoreDigit x
+  | 48 <= x && x <= 57  = x - 48
+  | 65 <= x && x <= 70  = x - 55
+  | 97 <= x && x <= 102 = x - 87
+  | otherwise           = error "Text.URI.Parser.restoreDigit: bad input"
diff --git a/Text/URI/Parser/Text.hs b/Text/URI/Parser/Text.hs
new file mode 100644
--- /dev/null
+++ b/Text/URI/Parser/Text.hs
@@ -0,0 +1,150 @@
+-- |
+-- Module      :  Text.URI.Parser.Text
+-- Copyright   :  © 2017 Mark Karpov
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- URI parser for strict 'Text', an internal module.
+
+{-# LANGUAGE DataKinds          #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric      #-}
+{-# LANGUAGE FlexibleContexts   #-}
+{-# LANGUAGE OverloadedStrings  #-}
+{-# LANGUAGE RankNTypes         #-}
+{-# LANGUAGE RecordWildCards    #-}
+
+module Text.URI.Parser.Text
+  ( mkURI
+  , parser
+  , ParseException (..) )
+where
+
+import Control.Applicative
+import Control.Monad
+import Control.Monad.Catch (Exception (..), MonadThrow (..))
+import Data.Data (Data)
+import Data.Maybe (isNothing, catMaybes)
+import Data.Text (Text)
+import Data.Typeable (Typeable)
+import Data.Void
+import GHC.Generics
+import Text.Megaparsec
+import Text.Megaparsec.Char
+import Text.URI.Parser.Text.Utils
+import Text.URI.Types
+import qualified Data.ByteString.Char8      as B8
+import qualified Data.List.NonEmpty         as NE
+import qualified Data.Text.Encoding         as TE
+import qualified Text.Megaparsec.Char.Lexer as L
+
+-- | Construct a 'URI' from 'Text'. In case of failure 'ParseException' is
+-- thrown.
+--
+-- This function uses the 'parser' parser under the hood, which you can also
+-- use directly in a Megaparsec parser.
+
+mkURI :: MonadThrow m => Text -> m URI
+mkURI input =
+  case runParser (parser <* eof :: Parsec Void Text URI) "" input of
+    Left err -> throwM (ParseException input err)
+    Right x  -> return x
+
+-- | Parse exception thrown by 'mkURI' when a given 'Text' value cannot be
+-- parsed as a 'URI'.
+
+data ParseException = ParseException Text (ParseError Char Void)
+  -- ^ Arguments are: original input and parse error
+  deriving (Show, Eq, Data, Typeable, Generic)
+
+instance Exception ParseException where
+  displayException (ParseException s e) = parseErrorPretty' s e
+
+-- | This parser can be used to parse 'URI' from strict 'Text'. Remember to
+-- use a concrete non-polymorphic parser type for efficiency.
+
+parser :: MonadParsec e Text m => m URI
+parser = do
+  uriScheme    <- optional (try pScheme)
+  uriAuthority <- optional pAuthority
+  uriPath      <- pPath (isNothing uriAuthority)
+  uriQuery     <- option [] pQuery
+  uriFragment  <- optional pFragment
+  return URI {..}
+{-# INLINEABLE parser #-}
+
+pScheme :: MonadParsec e Text m => m (RText 'Scheme)
+pScheme = do
+  x  <- asciiAlphaChar
+  xs <- many (asciiAlphaNumChar <|> char '+' <|> char '-' <|> char '.')
+  void (char ':')
+  liftR mkScheme (x:xs)
+{-# INLINE pScheme #-}
+
+pAuthority :: MonadParsec e Text m => m Authority
+pAuthority = do
+  void (string "//")
+  authUserInfo <- optional pUserInfo
+  authHost <- pHost True >>= liftR mkHost
+  authPort <- optional (char ':' *> L.decimal)
+  return Authority {..}
+{-# INLINE pAuthority #-}
+
+pUserInfo :: MonadParsec e Text m => m UserInfo
+pUserInfo = try $ do
+  uiUsername <- label "username" $
+    many (unreservedChar <|> percentEncChar <|> subDelimChar)
+      >>= liftR mkUsername
+  uiPassword <- optional $ do
+    void (char ':')
+    many (unreservedChar <|> percentEncChar <|> subDelimChar <|> char ':')
+      >>= liftR mkPassword
+  void (char '@')
+  return UserInfo {..}
+{-# INLINE pUserInfo #-}
+
+pPath :: MonadParsec e Text m => Bool -> m [RText 'PathPiece]
+pPath hadNoAuth = do
+  doubleSlash <- lookAhead (True <$ string "//" <|> pure False)
+  when (doubleSlash && hadNoAuth) $
+    (unexpected . Tokens . NE.fromList) "//"
+  path <- flip sepBy (char '/') . label "path piece" $
+    many pchar
+  mapM (liftR mkPathPiece) (filter (not . null) path)
+{-# INLINE pPath #-}
+
+pQuery :: MonadParsec e Text m => m [QueryParam]
+pQuery = do
+  void (char '?')
+  fmap catMaybes . flip sepBy (char '&') . label "query parameter" $ do
+    let p = many (pchar' <|> char '/' <|> char '?')
+    k' <- p
+    mv <- optional (char '=' *> p)
+    k  <- liftR mkQueryKey k'
+    if null k'
+      then return Nothing
+      else Just <$> case mv of
+             Nothing -> return (QueryFlag k)
+             Just v  -> QueryParam k <$> liftR mkQueryValue v
+{-# INLINE pQuery #-}
+
+pFragment :: MonadParsec e Text m => m (RText 'Fragment)
+pFragment = do
+  void (char '#')
+  xs <- many . label "fragment character" $
+    pchar <|> char '/' <|> char '?'
+  liftR mkFragment xs
+{-# INLINE pFragment #-}
+
+----------------------------------------------------------------------------
+-- Helpers
+
+liftR :: MonadParsec e s m
+  => (forall n. MonadThrow n => Text -> n r)
+  -> String
+  -> m r
+liftR f = maybe empty return . f . TE.decodeUtf8 . B8.pack
+{-# INLINE liftR #-}
diff --git a/Text/URI/Parser/Text/Utils.hs b/Text/URI/Parser/Text/Utils.hs
new file mode 100644
--- /dev/null
+++ b/Text/URI/Parser/Text/Utils.hs
@@ -0,0 +1,165 @@
+-- |
+-- Module      :  Text.URI.Parser.Text.Utils
+-- Copyright   :  © 2017 Mark Karpov
+-- License     :  BSD 3 clause
+--
+-- Maintainer  :  Mark Karpov <markkarpov92@gmail.com>
+-- Stability   :  experimental
+-- Portability :  portable
+--
+-- Random utilities for our 'Text' parsers.
+
+{-# LANGUAGE FlexibleContexts  #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Text.URI.Parser.Text.Utils
+  ( pHost
+  , asciiAlphaChar
+  , asciiAlphaNumChar
+  , unreservedChar
+  , percentEncChar
+  , subDelimChar
+  , pchar
+  , pchar' )
+where
+
+import Control.Monad
+import Data.Char
+import Data.List (intercalate)
+import Data.Maybe (maybeToList)
+import Data.Text (Text)
+import Text.Megaparsec
+import Text.Megaparsec.Char
+import qualified Data.List.NonEmpty         as NE
+import qualified Data.Set                   as E
+import qualified Data.Text                  as T
+import qualified Text.Megaparsec.Char.Lexer as L
+
+-- | Parser that can parse host names.
+
+pHost :: MonadParsec e Text m
+  => Bool              -- ^ Demand percent-encoding in reg names
+  -> m String
+pHost pe = choice
+  [ try (asConsumed ipLiteral)
+  , try (asConsumed ipv4Address)
+  , regName ]
+  where
+    asConsumed :: MonadParsec e Text m => m a -> m String
+    asConsumed p = T.unpack . fst <$> match p
+    ipLiteral = between (char '[') (char ']') $
+      try ipv6Address <|> ipvFuture
+    octet = do
+      pos       <- getNextTokenPosition
+      (toks, x) <- match L.decimal
+      when (x >= (256 :: Integer)) $ do
+        mapM_ setPosition pos
+        failure
+          (fmap Tokens . NE.nonEmpty . T.unpack $ toks)
+          (E.singleton . Label . NE.fromList $ "decimal number from 0 to 255")
+    ipv4Address =
+      count 3 (octet <* char '.') *> octet
+    ipv6Address = do
+      pos        <- getNextTokenPosition
+      (toks, xs) <- match $ do
+        xs' <- maybeToList <$> optional ([] <$ string "::")
+        xs  <- flip sepBy1 (char ':') $ do
+          (skip, hasMore) <- lookAhead . hidden $ do
+            skip    <- option False (True <$ char ':')
+            hasMore <- option False (True <$ hexDigitChar)
+            return (skip, hasMore)
+          case (skip, hasMore) of
+            (True,  True)  -> return []
+            (True,  False) -> [] <$ char ':'
+            (False, _)     -> count' 1 4 hexDigitChar
+        return (xs' ++ xs)
+      let nskips  = length (filter null xs)
+          npieces = length xs
+      unless (nskips < 2 && (npieces == 8 || (nskips == 1 && npieces < 8))) $ do
+        mapM_ setPosition pos
+        failure
+          (fmap Tokens . NE.nonEmpty . T.unpack $ toks)
+          (E.singleton . Label . NE.fromList $ "valid IPv6 address")
+    ipvFuture = do
+      void (char 'v')
+      void hexDigitChar
+      void (char '.')
+      skipSome (unreservedChar <|> subDelimChar <|> char ':')
+    regName = fmap (intercalate ".") . flip sepBy1 (char '.') $ do
+      let ch =
+            if pe
+              then percentEncChar <|> asciiAlphaNumChar
+              else alphaNumChar
+      x <- ch
+      let r = ch <|> try
+            (char '-' <* (lookAhead . try) (ch <|> char '-'))
+      xs <- many r
+      return (x:xs)
+{-# INLINEABLE pHost #-}
+
+-- | Parse an ASCII alpha character.
+
+asciiAlphaChar :: MonadParsec e Text m => m Char
+asciiAlphaChar = satisfy isAsciiAlpha <?> "ASCII alpha character"
+{-# INLINE asciiAlphaChar #-}
+
+-- | Parse an ASCII alpha-numeric character.
+
+asciiAlphaNumChar :: MonadParsec e Text m => m Char
+asciiAlphaNumChar = satisfy isAsciiAlphaNum <?> "ASCII alpha-numeric character"
+{-# INLINE asciiAlphaNumChar #-}
+
+-- | Parse an unreserved character.
+
+unreservedChar :: MonadParsec e Text m => m Char
+unreservedChar = label "unreserved character" . satisfy $ \x ->
+  isAsciiAlphaNum x || x == '-' || x == '.' || x == '_' || x == '~'
+{-# INLINE unreservedChar #-}
+
+-- | Parse a percent-encoded character.
+
+percentEncChar :: MonadParsec e Text m => m Char
+percentEncChar = do
+  void (char '%')
+  h <- digitToInt <$> hexDigitChar
+  l <- digitToInt <$> hexDigitChar
+  return . chr $ h * 16 + l
+{-# INLINE percentEncChar #-}
+
+-- | Parse a sub-delimiter character.
+
+subDelimChar :: MonadParsec e Text m => m Char
+subDelimChar = oneOf s <?> "sub-delimiter"
+  where
+    s = E.fromList "!$&'()*+,;="
+{-# INLINE subDelimChar #-}
+
+-- | PCHAR thing from the spec.
+
+pchar :: MonadParsec e Text m => m Char
+pchar = choice
+  [ unreservedChar
+  , percentEncChar
+  , subDelimChar
+  , char ':'
+  , char '@' ]
+{-# INLINE pchar #-}
+
+-- | 'pchar' adjusted for query parsing.
+
+pchar' :: MonadParsec e Text m => m Char
+pchar' = choice
+  [ unreservedChar
+  , percentEncChar
+  , oneOf s <?> "sub-delimiter"
+  , char ':'
+  , char '@' ]
+  where
+    s = E.fromList "!$'()*+,;"
+{-# INLINE pchar' #-}
+
+isAsciiAlpha :: Char -> Bool
+isAsciiAlpha x = isAscii x && isAlpha x
+
+isAsciiAlphaNum :: Char -> Bool
+isAsciiAlphaNum x = isAscii x && isAlphaNum x
diff --git a/Text/URI/QQ.hs b/Text/URI/QQ.hs
--- a/Text/URI/QQ.hs
+++ b/Text/URI/QQ.hs
@@ -33,7 +33,7 @@
 import Language.Haskell.TH
 import Language.Haskell.TH.Quote (QuasiQuoter (..))
 import Language.Haskell.TH.Syntax (lift)
-import Text.URI.Parser
+import Text.URI.Parser.Text
 import Text.URI.Types
 import qualified Data.Text as T
 
diff --git a/Text/URI/Render.hs b/Text/URI/Render.hs
--- a/Text/URI/Render.hs
+++ b/Text/URI/Render.hs
@@ -20,7 +20,9 @@
   ( render
   , render'
   , renderBs
-  , renderBs' )
+  , renderBs'
+  , renderStr
+  , renderStr' )
 where
 
 import Data.ByteString (ByteString)
@@ -31,10 +33,12 @@
 import Data.String (IsString (..))
 import Data.Text (Text)
 import Data.Word (Word8)
+import Numeric (showInt)
 import Text.URI.Types
 import qualified Data.ByteString              as B
 import qualified Data.ByteString.Lazy         as BL
 import qualified Data.ByteString.Lazy.Builder as BLB
+import qualified Data.Semigroup               as S
 import qualified Data.Text                    as T
 import qualified Data.Text.Encoding           as TE
 import qualified Data.Text.Lazy               as TL
@@ -55,7 +59,7 @@
 render' = genericRender TLB.decimal $ \e ->
   TLB.fromText . percentEncode e . unRText
 
--- | Render a given 'URI.Normalized' value as a strict 'ByteString'.
+-- | Render a given 'URI' value as a strict 'ByteString'.
 
 renderBs :: URI -> ByteString
 renderBs = BL.toStrict . BLB.toLazyByteString . renderBs'
@@ -66,6 +70,21 @@
 renderBs' = genericRender BLB.wordDec $ \e ->
   BLB.byteString . TE.encodeUtf8 . percentEncode e . unRText
 
+-- | Render a given 'URI' value as a 'String'.
+--
+-- @since 0.0.2.0
+
+renderStr :: URI -> String
+renderStr = ($ []) . renderStr'
+
+-- | Render a given 'URI' value as 'ShowS'.
+--
+-- @since 0.0.2.0
+
+renderStr' :: URI -> ShowS
+renderStr' = toShowS . genericRender (DString . showInt) (\e ->
+  fromString . T.unpack . percentEncode e . unRText)
+
 ----------------------------------------------------------------------------
 -- Generic render
 
@@ -126,6 +145,21 @@
 rFragment :: R b => Render (RText 'Fragment) b
 rFragment r = ("#" <>) . r P
 {-# INLINE rFragment #-}
+
+----------------------------------------------------------------------------
+-- DString
+
+newtype DString = DString { toShowS :: ShowS }
+
+instance S.Semigroup DString where
+  DString a <> DString b = DString (a . b)
+
+instance Monoid DString where
+  mempty = DString id
+  mappend = (S.<>)
+
+instance IsString DString where
+  fromString str = DString (str ++)
 
 ----------------------------------------------------------------------------
 -- Percent-encoding
diff --git a/Text/URI/Types.hs b/Text/URI/Types.hs
--- a/Text/URI/Types.hs
+++ b/Text/URI/Types.hs
@@ -43,14 +43,13 @@
   , pHost )
 where
 
-import Control.Applicative
 import Control.DeepSeq
 import Control.Monad
 import Control.Monad.Catch (Exception (..), MonadThrow (..))
 import Data.Char
 import Data.Data (Data)
 import Data.List (intercalate)
-import Data.Maybe (fromMaybe, isJust, fromJust, maybeToList)
+import Data.Maybe (fromMaybe, isJust, fromJust)
 import Data.Proxy
 import Data.Text (Text)
 import Data.Typeable (Typeable)
@@ -61,10 +60,8 @@
 import Test.QuickCheck hiding (label)
 import Text.Megaparsec
 import Text.Megaparsec.Char
-import qualified Data.List.NonEmpty         as NE
-import qualified Data.Set                   as E
-import qualified Data.Text                  as T
-import qualified Text.Megaparsec.Char.Lexer as L
+import Text.URI.Parser.Text.Utils (pHost)
+import qualified Data.Text as T
 
 ----------------------------------------------------------------------------
 -- Data types
@@ -379,92 +376,6 @@
 
 ifMatches :: Parsec Void Text () -> Text -> Bool
 ifMatches p = isJust . parseMaybe p
-
--- | Parser for unreserved characters as per RFC3986.
-
-unreserved :: MonadParsec e Text m => m Char
-unreserved = label "unreserved character" . satisfy $ \x ->
-  isAlphaNum x || x == '-' || x == '.' || x == '_' || x == '~'
-
--- | Match a sub-delimiter.
-
-subDelim :: MonadParsec e Text m => m Char
-subDelim = oneOf s <?> "sub-delimiter"
-  where
-    s = E.fromList "!$&'()*+,;="
-
--- | Parser that can parse host names.
-
-pHost :: MonadParsec e Text m
-  => Bool              -- ^ Demand percent-encoding in reg names
-  -> m String
-pHost pe = choice
-  [ try (asConsumed ipLiteral)
-  , try (asConsumed ipv4Address)
-  , regName ]
-  where
-    asConsumed :: MonadParsec e Text m => m a -> m String
-    asConsumed p = T.unpack . fst <$> match p
-    ipLiteral = between (char '[') (char ']') $
-      try ipv6Address <|> ipvFuture
-    octet = do
-      pos       <- getNextTokenPosition
-      (toks, x) <- match L.decimal
-      when (x >= (256 :: Integer)) $ do
-        mapM_ setPosition pos
-        failure
-          (fmap Tokens . NE.nonEmpty . T.unpack $ toks)
-          (E.singleton . Label . NE.fromList $ "decimal number from 0 to 255")
-    ipv4Address =
-      count 3 (octet <* char '.') *> octet
-    ipv6Address = do
-      pos        <- getNextTokenPosition
-      (toks, xs) <- match $ do
-        xs' <- maybeToList <$> optional ("" <$ string "::")
-        xs  <- flip sepBy1 (char ':') $ do
-          (skip, hasMore) <- lookAhead . hidden $ do
-            skip    <- option False (True <$ char ':')
-            hasMore <- option False (True <$ hexDigitChar)
-            return (skip, hasMore)
-          case (skip, hasMore) of
-            (True,  True)  -> return ""
-            (True,  False) -> "" <$ char ':'
-            (False, _)     -> count' 1 4 hexDigitChar
-        return (xs' ++ xs)
-      let nskips  = length (filter null xs)
-          npieces = length xs
-      unless (nskips < 2 && (npieces == 8 || (nskips == 1 && npieces < 8))) $ do
-        mapM_ setPosition pos
-        failure
-          (fmap Tokens . NE.nonEmpty . T.unpack $ toks)
-          (E.singleton . Label . NE.fromList $ "valid IPv6 address")
-    ipvFuture = do
-      void (char 'v')
-      void hexDigitChar
-      void (char '.')
-      skipSome (unreserved <|> subDelim <|> char ':')
-    regName = fmap (intercalate ".") . flip sepBy1 (char '.') $ do
-      let ch =
-            if pe
-              then percentEncChar <|> asciiAlphaNumChar
-              else alphaNumChar
-      x <- ch
-      let r = ch <|> try
-            (char '-' <* (lookAhead . try) (ch <|> char '-'))
-      xs <- many r
-      return (x:xs)
-
-asciiAlphaNumChar :: MonadParsec e Text m => m Char
-asciiAlphaNumChar = satisfy f <?> "ASCII alpha-numeric character"
-  where
-    f x = isAscii x && isAlphaNum x
-
-percentEncChar :: MonadParsec e Text m => m Char
-percentEncChar = do
-  void (char '%')
-  h <- digitToInt <$> hexDigitChar
-  l <- digitToInt <$> hexDigitChar
-  return . chr $ h * 16 + l
 
 ----------------------------------------------------------------------------
 -- Arbitrary helpers
diff --git a/bench/memory/Main.hs b/bench/memory/Main.hs
--- a/bench/memory/Main.hs
+++ b/bench/memory/Main.hs
@@ -2,43 +2,56 @@
 
 module Main (main) where
 
+import Data.ByteString (ByteString)
 import Data.Maybe (fromJust)
+import Data.String (IsString (..))
 import Data.Text (Text)
 import Data.Void
 import Text.Megaparsec
 import Text.URI (URI)
 import Weigh
-import qualified Data.Text as T
-import qualified Text.URI  as URI
+import qualified Data.ByteString.Char8 as B8
+import qualified Data.Text             as T
+import qualified Text.URI              as URI
 
 main :: IO ()
 main = mainWith $ do
   setColumns [Case, Allocated, GCs, Max]
-  bparser   turiText
-  brender   turi
-  brenderBs turi
+  bparser    turiStr
+  bparserBs  turiStr
+  brender    turi
+  brenderBs  turi
+  brenderStr turi
 
 ----------------------------------------------------------------------------
 -- Helpers
 
--- | Test 'URI' as 'Text'.
+-- | Test 'URI' as a polymorphic string.
 
-turiText :: Text
-turiText = "https://mark:secret@github.com:443/mrkkrp/modern-uri?foo=bar#fragment"
+turiStr :: IsString a => a
+turiStr = "https://mark:secret@github.com:443/mrkkrp/modern-uri?foo=bar#fragment"
 
 -- | Test 'URI' in parsed form.
 
 turi :: URI
-turi = fromJust (URI.mkURI turiText)
+turi = fromJust (URI.mkURI turiStr)
 
 -- | Benchmark memory usage of the 'URI' parser with given input.
 
 bparser :: Text -> Weigh ()
 bparser txt = func name (parse p "") txt
   where
-    name = "parser: " ++ T.unpack txt
+    name = "text parser: " ++ T.unpack txt
     p    = URI.parser <* eof :: Parsec Void Text URI
 
+-- | Like 'bparser' but accepts a 'ByteString'.
+
+bparserBs :: ByteString -> Weigh ()
+bparserBs bs = func name (parse p "") bs
+  where
+    name = "bs   parser: " ++ B8.unpack bs
+    p    = URI.parserBs <* eof :: Parsec Void ByteString URI
+
 -- | Benchmark memory usage of the 'URI' render with given input.
 
 brender :: URI -> Weigh ()
@@ -46,9 +59,16 @@
   where
     name = "text render: " ++ T.unpack (URI.render uri)
 
--- | The same as 'brender' but for the 'ByteString' render.
+-- | The same as 'brender' but for the 'URI.renderBs' render.
 
 brenderBs :: URI -> Weigh ()
 brenderBs uri = func name URI.renderBs uri
   where
-    name = "bs render: " ++ T.unpack (URI.render uri)
+    name = "bs   render: " ++ T.unpack (URI.render uri)
+
+-- | The same as 'brender' but for the 'URI.renderString' render.
+
+brenderStr :: URI -> Weigh ()
+brenderStr uri = func name URI.renderStr uri
+  where
+    name = "str  render: " ++ T.unpack (URI.render uri)
diff --git a/bench/speed/Main.hs b/bench/speed/Main.hs
--- a/bench/speed/Main.hs
+++ b/bench/speed/Main.hs
@@ -3,41 +3,54 @@
 module Main (main) where
 
 import Criterion.Main
+import Data.ByteString (ByteString)
 import Data.Maybe (fromJust)
+import Data.String (IsString (..))
 import Data.Text (Text)
 import Data.Void
 import Text.Megaparsec
 import Text.URI (URI)
-import qualified Data.Text as T
-import qualified Text.URI  as URI
+import qualified Data.ByteString.Char8 as B8
+import qualified Data.Text             as T
+import qualified Text.URI              as URI
 
 main :: IO ()
 main = defaultMain
-  [ bparser   turiText
-  , brender   turi
-  , brenderBs turi ]
+  [ bparser    turiStr
+  , bparserBs  turiStr
+  , brender    turi
+  , brenderBs  turi
+  , brenderStr turi ]
 
 ----------------------------------------------------------------------------
 -- Helpers
 
--- | Test 'URI' as 'Text'.
+-- | Test 'URI' as a polymorphic string.
 
-turiText :: Text
-turiText = "https://mark:secret@github.com:443/mrkkrp/modern-uri?foo=bar#fragment"
+turiStr :: IsString a => a
+turiStr = "https://mark:secret@github.com:443/mrkkrp/modern-uri?foo=bar#fragment"
 
 -- | Test 'URI' in parsed form.
 
 turi :: URI
-turi = fromJust (URI.mkURI turiText)
+turi = fromJust (URI.mkURI turiStr)
 
 -- | Benchmark speed of the 'URI' parser with given input.
 
 bparser :: Text -> Benchmark
 bparser txt = env (return txt) (bench name . nf p)
   where
-    name = "parser: " ++ T.unpack txt
+    name = "text parser: " ++ T.unpack txt
     p    = parse (URI.parser <* eof :: Parsec Void Text URI) ""
 
+-- | Like 'bparser' but accepts a 'ByteString'.
+
+bparserBs :: ByteString -> Benchmark
+bparserBs bs = env (return bs) (bench name . nf p)
+  where
+    name = "bs   parser: " ++ B8.unpack bs
+    p    = parse (URI.parserBs <* eof :: Parsec Void ByteString URI) ""
+
 -- | Benchmark speed of the 'URI' render with given input.
 
 brender :: URI -> Benchmark
@@ -45,9 +58,16 @@
   where
     name = "text render: " ++ T.unpack (URI.render uri)
 
--- | The same as 'brender' but for the 'ByteString' render.
+-- | The same as 'brender' but for the 'URI.renderBs' render.
 
 brenderBs :: URI -> Benchmark
 brenderBs uri = env (return uri) (bench name . nf URI.renderBs)
   where
-    name = "bs render: " ++ T.unpack (URI.render uri)
+    name = "bs   render: " ++ T.unpack (URI.render uri)
+
+-- | The same as 'brender' but for the 'URI.renderString' render.
+
+brenderStr :: URI -> Benchmark
+brenderStr uri = env (return uri) (bench name . nf URI.renderStr)
+  where
+    name = "str  render: " ++ T.unpack (URI.render uri)
diff --git a/modern-uri.cabal b/modern-uri.cabal
--- a/modern-uri.cabal
+++ b/modern-uri.cabal
@@ -1,5 +1,5 @@
 name:                 modern-uri
-version:              0.0.1.0
+version:              0.0.2.0
 cabal-version:        >= 1.18
 tested-with:          GHC==7.10.3, GHC==8.0.2, GHC==8.2.1
 license:              BSD3
@@ -41,7 +41,9 @@
   exposed-modules:    Text.URI
                     , Text.URI.Lens
                     , Text.URI.QQ
-  other-modules:      Text.URI.Parser
+  other-modules:      Text.URI.Parser.ByteString
+                    , Text.URI.Parser.Text
+                    , Text.URI.Parser.Text.Utils
                     , Text.URI.Render
                     , Text.URI.Types
   if flag(dev)
@@ -56,6 +58,7 @@
   type:               exitcode-stdio-1.0
   build-depends:      QuickCheck       >= 2.4 && < 3.0
                     , base             >= 4.7 && < 5.0
+                    , bytestring       >= 0.2 && < 0.11
                     , hspec            >= 2.0 && < 3.0
                     , hspec-megaparsec >= 1.0 && < 2.0
                     , megaparsec       >= 6.0 && < 7.0
@@ -73,6 +76,7 @@
   hs-source-dirs:     bench/speed
   type:               exitcode-stdio-1.0
   build-depends:      base             >= 4.7 && < 5.0
+                    , bytestring       >= 0.2 && < 0.11
                     , criterion        >= 0.6.2.1 && < 1.3
                     , megaparsec       >= 6.0 && < 7.0
                     , modern-uri
diff --git a/tests/Text/URISpec.hs b/tests/Text/URISpec.hs
--- a/tests/Text/URISpec.hs
+++ b/tests/Text/URISpec.hs
@@ -3,6 +3,7 @@
 
 module Text.URISpec (spec) where
 
+import Data.ByteString (ByteString)
 import Data.Maybe (isNothing, isJust)
 import Data.Text (Text)
 import Data.Void
@@ -128,6 +129,10 @@
     it "parser and render are consistent" $
       property $ \uri ->
         shouldParse' (URI.render uri) uri
+  describe "parseBs and renderBs" $
+    it "parser and render are consistent" $
+      property $ \uri ->
+        shouldParseBs (URI.renderBs uri) uri
   describe "parse" $ do
     it "rejects Unicode in scheme" $
       parse urip "" "что:something" `shouldFailWith` err posI (mconcat
@@ -165,6 +170,10 @@
     it "sort of works" $
       fmap URI.renderBs mkTestURI `shouldReturn`
         "https://mark:secret@github.com:443/mrkkrp/modern-uri?foo=bar#fragment"
+  describe "renderStr" $
+    it "sort of works" $
+      fmap URI.renderStr mkTestURI `shouldReturn`
+        "https://mark:secret@github.com:443/mrkkrp/modern-uri?foo=bar#fragment"
 
 ----------------------------------------------------------------------------
 -- Helpers
@@ -218,6 +227,19 @@
   -> Expectation
 shouldParse' s a =
   case runParser urip "" s of
+    Left e -> expectationFailure $
+      "the parser is expected to succeed, but it failed with:\n" ++
+      parseErrorPretty' s e
+    Right a' -> a' `shouldBe` a
+
+-- | Similar to 'shouldParse'' but uses 'URI.parserBs' under the hood.
+
+shouldParseBs
+  :: ByteString        -- ^ Parser input
+  -> URI               -- ^ 'URI' to compare with
+  -> Expectation
+shouldParseBs s a =
+  case runParser (URI.parserBs <* eof :: Parsec Void ByteString URI) "" s of
     Left e -> expectationFailure $
       "the parser is expected to succeed, but it failed with:\n" ++
       parseErrorPretty' s e
