Are you sure you have to initialize every part of your app at the very start of your application?
One of the most common things done at the application start is initializing various resources. This can be app config, logging service, or some database connection that will be later used by the request handlers. However, not all of these need to be ready at the very start of the app, and can lead to a slow-start.
This is where lazy initialization helps us with deferring the resource initialization until the resource is needed. This can also skip the initialization entirely if the resource in question is not used at all.
In older versions, Rust did not have support for such lazy initializations in its standard library. There were several popular crates in the ecosystem which were commonly used for this functionality, such as lazy_static
and once_cell
. Starting from Rust 1.80, a lot of functionality provided by these crates is now available in the standard library itself, and can be used in place of these two crates.
In this post, we will see:
lazy_static
and once_cell provide lazy initializationlazy_static
, and once_cell
Consider an example where we have an API endpoint which is infrequently used and needs to read and parse a large file from disk.
We can do the read and parse at the start of our application, however, that might block the server from serving requests until the parsing is complete. It might also be the case that the particular API endpoint is not hit at all, so the resources spent in loading the file were not useful.
Another example is if an application uses some in-memory DB such as sqlite or redis. However, not all invocation of the application actually need the DB. In such a case, loading the DB in memory and maintaining the connection overhead each time is not useful.
We can defer the initialization of such resources until they are needed, initialize them on their first use, and keep them around for later use. This pattern is known as lazy initialization.
However this presents a small problem in Rust , where we must either pass the lazily initialized resource to each function as a param, or make it a static global and use unsafe Rust to initialize it at runtime.
To avoid this, crates like lazy_static
or once_cell
provide safe wrappers around the unsafe operations, and we can use them to safely use lazily initialized values throughout our code.
lazy_static
and once_cell
provide lazy initializationlazy_static
provides a macro to write the initialization code for the static variables, and it is used at runtime to initialize the variable at the first use. The general syntax is:
use lazy_static::lazy_static; lazy_static!{ static ref VAR : TYPE = {initialization code} }
For example, setting the log level as a static variable would be something like:
use lazy_static::lazy_static; lazy_static! { static ref LOG_LEVEL: String = get_log_level(); } fn get_log_level() -> String { match std::env::var("LOG_LEVEL") { Ok(s) => s, Err(_) => "WARN".to_string(), } } fn main() { println!("{}", *LOG_LEVEL); }
Code in the lazy_static!
definition uses the get_log_level
function at runtime to set the log level.
While pretty straightforward, this has some of its own quirks. We have to use static ref
, which is not a valid Rust syntax, and we need to de-reference the LOG_LEVEL
for use as seen in the println
statement.
We can do the same thing using once_cell
crate as:
use once_cell::sync::OnceCell; static LOG_LEVEL: OnceCell<String> = OnceCell::new(); fn get_log_level() -> String { match std::env::var("LOG_LEVEL") { Ok(s) => s, Err(_) => "WARN".to_string(), } } fn main() { let log_level = LOG_LEVEL.get_or_init(get_log_level); println!("{}", log_level); }
Here, instead of specifying the code in the declaration, we use the get_or_init
method when we want to retrieve the value.
If the value is not initialized, the given function will be used to initialize the value, and otherwise it will return the existing value. Because we get the value directly, we do not need any extra de-referencing.
While both of these have their pros and cons, one common thing is that you need to add one more external crate in your dependencies. Now that the stdlib provides types for the lazy pattern, we can directly use those instead and reduce the number of dependencies.
In Rust 1.70 and 1.80, types similar to lazy_static
and once_cell
have been stabilized in the Rust Standard Library itself. We can use them instead of any external crates to achieve similar functionality.
OnceLock
type from the standard library can be used similar to once_cell
crate’s OnceCell
type:
use std::sync::OnceLock; static LOG_LEVEL: OnceLock<String> = OnceLock::new(); fn get_log_level() -> String { match std::env::var("LOG_LEVEL") { Ok(s) => s, Err(_) => "WARN".to_string(), } } fn main() { let log_level = LOG_LEVEL.get_or_init(get_log_level); println!("{}", log_level); }
Comparing to the once_cell
example, we have replaced the OnceCell
by OnceLock
, but the rest of the code is still the same. OnceLock
type also exposes a method called get_or_init
which provides the same functionality as OnceCell
‘s get_or_init
.
Somewhat similar to the lazy_static
, we can use the LazyLock
type to specify the initialization function at the declaration level without having to use a macro:
use std::sync::LazyLock; static LOG_LEVEL: LazyLock<String> = LazyLock::new(get_log_level); fn get_log_level() -> String { match std::env::var("LOG_LEVEL") { Ok(s) => s, Err(_) => "WARN".to_string(), } } fn main() { println!("{}", *LOG_LEVEL); }
Here we pass an initialization function to the new
method of LazyLock
, and when the variable’s value is accessed for the first time, the type internally calls this function to initialize the value.
lazy_static
, once_cell
, and native typesAs mentioned in the release notes of Rust 1.70 and 1.80 , the code for the newly added native types is adopted from the once_cell
crate. Thus, they provide functionality quite similar to the original implementations from the crate.
The lazy_static
crate uses its own macro syntax to declare the static variables, which I still occasionally forget. Comparatively, once_cell
does not need any macro or custom syntax, and does all the lazy stuff based solely on types.
Another case to note is that in case of lazy_static
, the initialization code has to be directly written inside the declaration macro. If what you need is a more flexible way to initialize similar to get_or_init
function, you have to use once_cell
or the standard library types.
Compared to both of these crates, one big advantage of native types is not requiring any additional dependency. Even though both of the crates only have a couple of dependencies themselves, it still means your project will have a couple of more dependencies and take slightly more time for compiling.
Another advantage of native types is that they are developed and maintained directly by the official rust standard library team.
The introduction of lazy_static
initialization types into the Rust standard library itself provides a lot of convenience for using lazy initialization into our code without having to add another crate as a dependency.
With these types, we get the power of doing expensive calculation only when needed and initializing some expensive constructs such as regular expressions exactly once without having to deal with manual initialization checks every time.
That said, existing crates like lazy_static
or once_cell
are still popular and well-maintained, and I don’t see them going away anytime soon.
If you are already using them or want to use them because of familiarity, you can keep using them without worry. Even if you switch your own code to use the native types, there’s a good chance that some of your dependencies use these crates, and they may not switch to native types immediately, thus still keeping these crates in your dependency tree.
However, I believe we’ll see more use of the native types as time goes on instead of the external crates in new projects that are created after this.
You can find the code examples in the GitHub repo here. Thanks for reading!
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowValidating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.
Learn how Remix enhances SSR performance, simplifies data fetching, and improves SEO compared to client-heavy React apps.