diff --git a/Cargo.lock b/Cargo.lock
index 1844e4e8..1ca11497 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -143,7 +143,6 @@ dependencies = [
  "aho-corasick",
  "bstr",
  "glob",
- "lazy_static",
  "log",
  "regex-automata",
  "regex-syntax",
@@ -243,12 +242,10 @@ dependencies = [
  "crossbeam-channel",
  "crossbeam-deque",
  "globset",
- "lazy_static",
  "log",
  "memchr",
- "regex",
+ "regex-automata",
  "same-file",
- "thread_local",
  "walkdir",
  "winapi-util",
 ]
@@ -288,12 +285,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "lazy_static"
-version = "1.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
-
 [[package]]
 name = "libc"
 version = "0.2.148"
@@ -346,12 +337,6 @@ dependencies = [
  "libm",
 ]
 
-[[package]]
-name = "once_cell"
-version = "1.18.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
-
 [[package]]
 name = "packed_simd"
 version = "0.3.9"
@@ -364,14 +349,13 @@ dependencies = [
 
 [[package]]
 name = "pcre2"
-version = "0.2.4"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "486aca7e74edb8cab09a48d461177f450a5cca3b55e61d139f7552190e2bbcf5"
+checksum = "9deb1d02d6a373ee392128ba86087352a986359f32a106e2e3b08cc90cc659c9"
 dependencies = [
  "libc",
  "log",
  "pcre2-sys",
- "thread_local",
 ]
 
 [[package]]
@@ -447,7 +431,6 @@ dependencies = [
  "grep",
  "ignore",
  "jemallocator",
- "lazy_static",
  "log",
  "serde",
  "serde_derive",
@@ -543,16 +526,6 @@ dependencies = [
  "unicode-width",
 ]
 
-[[package]]
-name = "thread_local"
-version = "1.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
-dependencies = [
- "cfg-if",
- "once_cell",
-]
-
 [[package]]
 name = "unicode-ident"
 version = "1.0.12"
diff --git a/Cargo.toml b/Cargo.toml
index ac4c9b44..fa697a22 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -52,7 +52,6 @@ members = [
 bstr = "1.6.0"
 grep = { version = "0.2.12", path = "crates/grep" }
 ignore = { version = "0.4.19", path = "crates/ignore" }
-lazy_static = "1.1.0"
 log = "0.4.5"
 serde_json = "1.0.23"
 termcolor = "1.1.0"
@@ -65,9 +64,6 @@ features = ["suggestions"]
 [target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.jemallocator]
 version = "0.5.0"
 
-[build-dependencies]
-lazy_static = "1.1.0"
-
 [build-dependencies.clap]
 version = "2.33.0"
 default-features = false
diff --git a/crates/core/app.rs b/crates/core/app.rs
index 78c55963..ae97a2b5 100644
--- a/crates/core/app.rs
+++ b/crates/core/app.rs
@@ -10,7 +10,6 @@
 // it into a ripgrep-specific configuration type that is not coupled with clap.
 
 use clap::{self, crate_authors, crate_version, App, AppSettings};
-use lazy_static::lazy_static;
 
 const ABOUT: &str = "
 ripgrep (rg) recursively searches the current directory for a regex pattern.
@@ -47,18 +46,19 @@ OPTIONS:
 
 /// Build a clap application parameterized by usage strings.
 pub fn app() -> App<'static, 'static> {
+    use std::sync::OnceLock;
+
     // We need to specify our version in a static because we've painted clap
     // into a corner. We've told it that every string we give it will be
     // 'static, but we need to build the version string dynamically. We can
     // fake the 'static lifetime with lazy_static.
-    lazy_static! {
-        static ref LONG_VERSION: String = long_version(None, true);
-    }
+    static LONG_VERSION: OnceLock<String> = OnceLock::new();
+    let long_version = LONG_VERSION.get_or_init(|| long_version(None, true));
 
     let mut app = App::new("ripgrep")
         .author(crate_authors!())
         .version(crate_version!())
-        .long_version(LONG_VERSION.as_str())
+        .long_version(long_version.as_str())
         .about(ABOUT)
         .max_term_width(100)
         .setting(AppSettings::UnifiedHelpMessage)
diff --git a/crates/globset/Cargo.toml b/crates/globset/Cargo.toml
index 574db754..b0602239 100644
--- a/crates/globset/Cargo.toml
+++ b/crates/globset/Cargo.toml
@@ -37,7 +37,6 @@ features = ["std", "perf", "syntax", "meta", "nfa", "hybrid"]
 
 [dev-dependencies]
 glob = "0.3.1"
-lazy_static = "1"
 serde_json = "1.0.107"
 
 [features]
diff --git a/crates/ignore/Cargo.toml b/crates/ignore/Cargo.toml
index 3696f630..31771f17 100644
--- a/crates/ignore/Cargo.toml
+++ b/crates/ignore/Cargo.toml
@@ -21,14 +21,16 @@ bench = false
 [dependencies]
 crossbeam-deque = "0.8.3"
 globset = { version = "0.4.10", path = "../globset" }
-lazy_static = "1.1"
 log = "0.4.20"
 memchr = "2.6.3"
-regex = { version = "1.9.5", default-features = false, features = ["perf", "std", "unicode-gencat"] }
 same-file = "1.0.6"
-thread_local = "1"
 walkdir = "2.4.0"
 
+[dependencies.regex-automata]
+version = "0.3.8"
+default-features = false
+features = ["std", "perf", "syntax", "meta", "nfa", "hybrid", "dfa-onepass"]
+
 [target.'cfg(windows)'.dependencies.winapi-util]
 version = "0.1.2"
 
diff --git a/crates/ignore/src/gitignore.rs b/crates/ignore/src/gitignore.rs
index 23e024b6..da007298 100644
--- a/crates/ignore/src/gitignore.rs
+++ b/crates/ignore/src/gitignore.rs
@@ -8,7 +8,6 @@ the `git` command line tool.
 */
 
 use std::{
-    cell::RefCell,
     fs::File,
     io::{BufRead, BufReader, Read},
     path::{Path, PathBuf},
@@ -17,8 +16,7 @@ use std::{
 
 use {
     globset::{Candidate, GlobBuilder, GlobSet, GlobSetBuilder},
-    regex::bytes::Regex,
-    thread_local::ThreadLocal,
+    regex_automata::util::pool::Pool,
 };
 
 use crate::{
@@ -86,7 +84,7 @@ pub struct Gitignore {
     globs: Vec<Glob>,
     num_ignores: u64,
     num_whitelists: u64,
-    matches: Option<Arc<ThreadLocal<RefCell<Vec<usize>>>>>,
+    matches: Option<Arc<Pool<Vec<usize>>>>,
 }
 
 impl Gitignore {
@@ -253,8 +251,7 @@ impl Gitignore {
             return Match::None;
         }
         let path = path.as_ref();
-        let _matches = self.matches.as_ref().unwrap().get_or_default();
-        let mut matches = _matches.borrow_mut();
+        let mut matches = self.matches.as_ref().unwrap().get();
         let candidate = Candidate::new(path);
         self.set.matches_candidate_into(&candidate, &mut *matches);
         for &i in matches.iter().rev() {
@@ -346,7 +343,7 @@ impl GitignoreBuilder {
             globs: self.globs.clone(),
             num_ignores: nignore as u64,
             num_whitelists: nwhite as u64,
-            matches: Some(Arc::new(ThreadLocal::default())),
+            matches: Some(Arc::new(Pool::new(|| vec![]))),
         })
     }
 
@@ -596,23 +593,28 @@ fn excludes_file_default() -> Option<PathBuf> {
 /// Extract git's `core.excludesfile` config setting from the raw file contents
 /// given.
 fn parse_excludes_file(data: &[u8]) -> Option<PathBuf> {
+    use std::sync::OnceLock;
+
+    use regex_automata::{meta::Regex, util::syntax};
+
     // N.B. This is the lazy approach, and isn't technically correct, but
     // probably works in more circumstances. I guess we would ideally have
     // a full INI parser. Yuck.
-    lazy_static::lazy_static! {
-        static ref RE: Regex = Regex::new(
-            r"(?xim-u)
-            ^[[:space:]]*excludesfile[[:space:]]*
-            =
-            [[:space:]]*(.+)[[:space:]]*$
-            "
-        ).unwrap();
-    };
-    let caps = match RE.captures(data) {
-        None => return None,
-        Some(caps) => caps,
-    };
-    std::str::from_utf8(&caps[1]).ok().map(|s| PathBuf::from(expand_tilde(s)))
+    static RE: OnceLock<Regex> = OnceLock::new();
+    let re = RE.get_or_init(|| {
+        Regex::builder()
+            .configure(Regex::config().utf8_empty(false))
+            .syntax(syntax::Config::new().utf8(false))
+            .build(r"(?im-u)^\s*excludesfile\s*=\s*(\S+)\s*$")
+            .unwrap()
+    });
+    // We don't care about amortizing allocs here I think. This should only
+    // be called ~once per traversal or so? (Although it's not guaranteed...)
+    let mut caps = re.create_captures();
+    re.captures(data, &mut caps);
+    let span = caps.get_group(1)?;
+    let candidate = &data[span];
+    std::str::from_utf8(candidate).ok().map(|s| PathBuf::from(expand_tilde(s)))
 }
 
 /// Expands ~ in file paths to the value of $HOME.
diff --git a/crates/ignore/src/types.rs b/crates/ignore/src/types.rs
index 9db2f4b3..308b784d 100644
--- a/crates/ignore/src/types.rs
+++ b/crates/ignore/src/types.rs
@@ -84,12 +84,11 @@ assert!(matcher.matched("y.cpp", false).is_whitelist());
 ```
 */
 
-use std::{cell::RefCell, collections::HashMap, path::Path, sync::Arc};
+use std::{collections::HashMap, path::Path, sync::Arc};
 
 use {
     globset::{GlobBuilder, GlobSet, GlobSetBuilder},
-    regex::Regex,
-    thread_local::ThreadLocal,
+    regex_automata::util::pool::Pool,
 };
 
 use crate::{default_types::DEFAULT_TYPES, pathutil::file_name, Error, Match};
@@ -178,7 +177,7 @@ pub struct Types {
     /// The set of all glob selections, used for actual matching.
     set: GlobSet,
     /// Temporary storage for globs that match.
-    matches: Arc<ThreadLocal<RefCell<Vec<usize>>>>,
+    matches: Arc<Pool<Vec<usize>>>,
 }
 
 /// Indicates the type of a selection for a particular file type.
@@ -232,7 +231,7 @@ impl Types {
             has_selected: false,
             glob_to_selection: vec![],
             set: GlobSetBuilder::new().build().unwrap(),
-            matches: Arc::new(ThreadLocal::default()),
+            matches: Arc::new(Pool::new(|| vec![])),
         }
     }
 
@@ -280,7 +279,7 @@ impl Types {
                 return Match::None;
             }
         };
-        let mut matches = self.matches.get_or_default().borrow_mut();
+        let mut matches = self.matches.get();
         self.set.matches_into(name, &mut *matches);
         // The highest precedent match is the last one.
         if let Some(&i) = matches.last() {
@@ -358,7 +357,7 @@ impl TypesBuilder {
             has_selected,
             glob_to_selection,
             set,
-            matches: Arc::new(ThreadLocal::default()),
+            matches: Arc::new(Pool::new(|| vec![])),
         })
     }
 
@@ -416,10 +415,7 @@ impl TypesBuilder {
     /// If `name` is `all` or otherwise contains any character that is not a
     /// Unicode letter or number, then an error is returned.
     pub fn add(&mut self, name: &str, glob: &str) -> Result<(), Error> {
-        lazy_static::lazy_static! {
-            static ref RE: Regex = Regex::new(r"^[\pL\pN]+$").unwrap();
-        };
-        if name == "all" || !RE.is_match(name) {
+        if name == "all" || !name.chars().all(|c| c.is_alphanumeric()) {
             return Err(Error::InvalidDefinition);
         }
         let (key, glob) = (name.to_string(), glob.to_string());