Actix web performance - static files vs REST service

Hi,
I'm building a web service with Actix and I'm having trouble to figure out what is the most efficient between serbing the same data from a static file with actix_files::Files or from a REST route with actix_web::get("/url") -> HttpResponse.

Currently I load data at server start, then I build some html views that I keep in memory as Strings and serve through a REST GET web service. But I wonder if it would be a better idea to write actual html files and serve them as static files.

My observation seems to show that static files are served faster, but I'm a little confused about that because I thought that serving static files require to do an IO operation for each request, so I would believe that serving data from memory through a REST service would be faster.

I can't know where the truth is. I'm curious about any suggestion.

Thank you !

Im not really familiar with actix files api, but i suppose your file need to be served in response to GET request no matter what you choose, since thats what the browser will send.

Secondly, actix is a fast webserver - so no matter what you choose i dont think it will be slow due to the server. Ask yourself if your usecase really require this level optimization and tryout and profile a minimal implementation of both approaches? If you dont need it - choose the one that is easy to understand and maintain

Lastly I dont think serving a rendered html page will be very RESTish, so if that's a critera, perhaps consider using a frontend framework to render the page and only send the actual data?

1 Like

Like you said, I don't know if I really "need" to consider that optimisation level. My question is maybe be more for curiosity sake.

Currently I load data from json at server start and I build a full website from it, html documents etc.

As I get everything built in memory at some point, and again like you said, pages will be served as responses to GET requests anyway, it seemed to me more logical to just keep in memory my Strings and serve them implementing a GET Responder service, because write them in a static memory location and then re-read them for each request with the actix Files facility looked like an unnecessary extra operation.

But, when I have a look to the brower's network console, I can see that,with cache disabled, the GET request to load my html document ( served by the actix_web::get service) is benchmarked around 60ms, while the static files like js css etc, are aroud 2ms, and in all cases payload size is around 500 bytes. That's a big difference and I can't explain it with my current knowledge...

Iteresting. Im afraid I dont got any simple answer for you. Profiling and timing the rendering logic and data lookup functions could perhaps give some clues to what it is thats taking time, and if its dependant on your implementation or the framework code.

https://nnethercote.github.io/perf-book/profiling.html

If the files are reasonably sized and your system has free memory, the files are probably staying cached in memory by the operating system.

What does the code to respond with the HTML look like?

1 Like

I didn't think of the OS cache, that could be the "why".

The service that serves the html page is written like this


#[get("/{pth:.*}")]
pub async fn page(
    website: web::Data<std::sync::Mutex<WebSite>>,
    pth: web::Path<PathBuf>,
) -> impl Responder {
    let website = website.lock().unwrap();
    let pth = pth.into_inner();

    match website.get_page_by_url(&pth) {
        Some(page) => HttpResponse::Ok().body(page.html.to_string()),
        None => HttpResponse::NotFound().body(format!("Not found {}", pth.display())),
    }
}

And the Website struct current implementation is as follows (sorry I copied the whole module here but I guess cut it in part wouldn't make much sense...


#[derive(Debug, Clone)]
pub struct WebSite {
    root_page: Page,
    pub static_files_manager: StaticFilesManager,
    templates: Vec<PageTemplate>,
    pages_index: HashMap<PathBuf, Page>,
}

impl WebSiteBuilder {
    pub fn from_json(json: &str) -> Self {
        serde_json::from_str(json).unwrap()
    }

    pub fn with_static_files_manager(
        &mut self,
        static_files_manager: StaticFilesManager,
    ) -> WebSite {
        WebSite {
            root_page: self.root_page.clone(),
            static_files_manager: {
                let mut static_files_manager = static_files_manager;
                static_files_manager.add_pathes(&self.assets_index);
                static_files_manager
            },
            templates: self.templates.clone(),
            pages_index: HashMap::new(),
        }
    }

    pub fn load(config: &AppConfig) -> WebSiteBuilder {
        let file_path = match &config.load {
            None => std::env::current_dir()
                .unwrap()
                .join("templates")
                .join("new_website.json"),
            Some(pth) => pth.clone(),
        };

        WebSiteBuilder::from_json(&std::fs::read_to_string(file_path).unwrap())
    }
}

impl WebSite {
    pub fn build(&mut self) -> Self {
        self.root_page.build_with_template(
            self.templates
                .iter()
                .find(|t| t.name == self.root_page.template_name)
                .expect("Page template not found")
                .clone(),
        );

        self.root_page.build_html();

        for p in self.root_page.sub_pages.iter_mut() {
            p.build_with_template(
                self.templates
                    .iter()
                    .find(|t| t.name == p.template_name)
                    .expect("Page template not found")
                    .clone(),
            );
            p.build_html();
        }

        self.build_pages_index(self.root_page.clone(), PathBuf::from("/"));
        self.clone()
    }

    fn build_pages_index(&mut self, root_page: Page, from_url: PathBuf) {
        let url = from_url.join(&root_page.metadata.url_slug);

        self.pages_index.insert(url.clone(), root_page.clone());

        for p in root_page.sub_pages {
            self.build_pages_index(p, url.clone());
        }
    }

    pub fn get_page_by_url(&self, url: &PathBuf) -> Option<&Page> {
        self.pages_index.get(&PathBuf::from("/").join(url))
    }
}

The Mutex might explain part of the difference too. You're also doing a couple allocations that I don't think the files version needs, though that shouldn't matter TOO much

1 Like

Thanks for poiting that out.

The reason of having chosen a Mutex based implementation is because I want this package to be a pure standalone binary, so no database. And it's basically a CMS so the website structure is fully mutable...

Anyway yes I guess that the addition of those little operations make that difference.

Also I'm not very experienced and I guess this could be done better :smile: .

If you're rendering the templates before the server starts it should be possible to just use an Arc and not a Mutex, though other things might make that harder.

At a minimum you probably want RwLock instead of Mutex, since multiple requests could need to retrieve a page at the same time. As long as they don't need to modify the pages in the HashMap there's no reason for them to need exclusive access.

1 Like

Well the Website can actually be modified during the life of the server running session (so the pages ressources would be rebuilt at this moment), so I think I can't handle it it with just an Arc, and I didn't succeed in give a kind of readonly access to the website pages when mutation is not in question.

But I'll give a try to RwLock. Thanks for tip.