Networking & Content Delivery
Improve Single-Page Application (SPA) Performance with a Same Domain policy using Amazon CloudFront
In this post, we demonstrate how you can use a same domain policy with Amazon CloudFront. Using this method, you eliminate the need for enabling Cross-Origin Resource Sharing (CORS), which results in improved performance for Single-Page Applications (SPA).
Over the years, SPA frameworks such as ReactJs and AngularJs have become a popular method for developing and hosting websites while limiting resource usage on a web server. An SPA is a web application that interacts with the user/client by dynamically rewriting the current web page with new data from the web server via APIs. This dramatically improves the website’s performance and provides a more dynamic user experience.
A common method for SPA deployments has been leveraging the use of Amazon Simple Storage Service (Amazon S3) to host the web application’s static assets, such as html, javascript, stylesheets, and images. The SPA is front ended by CloudFront to serve as scalable distribution. Furthermore, the front end would interact with a set of APIs exposed by Amazon API Gateway. See the following diagram for this architecture.
Figure 1. Architecture of a typical SPA hosted in AWS.
Before we dive into the performance challenges for this setup, let’s review some basic principles of CORS and preflight requests.
CORS and preflight requests
“CORS is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources”
CORS defines a method for client web applications that are loaded in one domain to interact with resources in a different domain. The mechanism that CORS uses for browsers to gain approval for resource utilization in another domain is via ‘preflight requests’. A CORS preflight request is a CORS request that checks to see if the CORS protocol is understood and a server is aware using specific methods and headers.
In the context of an SPA, the API calls are often made to a different domain that hosts all of the API endpoints for backend applications. This pattern requires enabling CORS on the APIs. The previous example SPA deployed in AWS requires enabling CORS on API Gateway because the API requests to backend applications are from a different origin than SPA.
One consequence of enabling CORS is a preflight request if the HTTP request isn’t a simple request.
One criteria for simple HTTP requests is that if a content type header is present, then it can only have values from text/plain
, multipart/form-data
or application/x-www-form-urlencoded
. Since REST API utilization is common, most application HTTP requests include the header content-type: application/json
, thus rendering the requests as non-simple.
In this setup, most HTTP requests will incur a preflight request before content can be served from backend applications. If the users accessing the website are in a different region than the API’s region, then the added latency for preflight requests can be significant and thereby degrade user experience due to poor performance.
The following diagram depicts the sequence of request and responses that occur for a non-simple HTTP request.
Figure 2. Sequence of requests and responses for CORS and non-simple http request.
One way to solve this issue is to make sure that the front-end and back-end component resources can be accessed from the same domain. This eliminates preflight requests and results in improved application performance. It can also be achieved by introducing a reverse proxy that can forward the requests to API Gateway. Instead of setting up proxy servers, we can leverage CloudFront’s multiple origin capability to solve this problem.
Using CloudFront to serve dynamic content from APIs
CloudFront is commonly used as a Content Delivery Network (CDN) and can also serve dynamic content from the application back end. Using CloudFront for this purpose eliminates the need for CORS preflight requests, since the entirety of the SPA is on the same origin. Note that the SPA also utilizes the CDN component by routing API requests to the closest point of presence using the AWS backbone network. This allows for further performance improvement (reduced latency) and thereby user experience.
Figure 3. Architecture depicting the use of CloudFront to serve data from API Gateway.
Automate the deployment with AWS Cloud Development Kit
The architecture depicted in the previous figure can be deployed automatically using AWS Cloud Development Kit (AWS CDK), which defines the cloud infrastructure. AWS CDK is a software development framework that uses programming languages for deployments with AWS CloudFormation. When an application runs, it compiles fully-formed CloudFormation JSON and YAML templates that are provisioned by the CloudFormation service.
We prepared a GitHub repository for you that contains an AWS CDK application written in TypeScript. To deploy it, complete the following steps:
Prerequisites
For the deployment, you must have the following:
- An AWS account. If you don’t have an AWS account, then sign up here.
- AWS CDK installed in your local environment.
- An S3 bucket for hosting SPA assets.
- CloudFront distribution.
- An AWS Lambda function.
- An API Gateway REST API with a proxy integration for the Lambda function.
Steps to deploy the package
1. Use your command-line shell to clone the GitHub repository.
git clone https://github.com/aws-samples/cloudfront-spa-with-samedomain-multiorigin
2. Navigate to the repository’s root directory.
cd cloudfront-spa-with-samedomain-multiorigin
3. Run npm install
to install node package dependencies.
4. If this is your first time deploying stack with AWS CDK in your AWS account, then run the following command to bootstrap your AWS environment.
cdk bootstrap
Note that bootstrapping launches resources into your AWS environment that are required by AWS CDK. These include an S3 bucket for storing files and AWS Identity and Access Management (IAM) roles that grant permissions needed to run our deployment.
5. Deploy the AWS CDK application.
cdk deploy
Let’s look at the configuration of the deployed resources. There are four main components deployed with the AWS CDK along with the required roles and permissions.
The following screenshot shows how there are two origins configured on the CloudFront distribution. The first origin is configured to serve from the deployed REST API, and the second origin serves files from the S3 bucket hosting the SPA.
Figure 4. Figure showing multiple origins configured on CloudFront distribution.
Additionally, there are two behaviors configured on the distribution. This configuration determines which client requests should be served from Amazon S3 as opposed to API Gateway.
Figure 5. Figure showing additional behavior configured on CloudFront distribution.
In this deployment, we disabled caching on the behavior for the `/api/*`
path to make sure that API requests are always served from API gateway. However, we also enabled caching on the default behavior that serves SPA assets. Note that this isn’t a prerequisite. If your backend requires caching at CloudFront, then you can choose to enable it on the behavior. Furthermore, we set the Origin request policy to “AllViewerExceptHostHeader”. This policy is intended for use with Amazon API Gateway origins. This origin expects the Host header to contain the origin domain name, not the domain name of the CloudFront distribution. Forwarding the Host header from the viewer request to these origins can prevent them from working.
Figure 6. Figure showing configured properties on additional behavior.
For the API Gateway endpoint, CORS isn’t enabled since we’re relying on CloudFront as the origin for all requests. Note that this setup doesn’t restrict you from enabling CORS. If you have other consumers of the APIs that have a different origin domain, then you can still choose to enable CORS.
The repository includes a sample SPA that you can optionally deploy to the provisioned S3 bucket using below steps.
- Run the following commands to build the angular application.
cd sample-spa npm install && ng build
- Copy all build files generated under dist/sample-spa to the root of the provisioned S3 bucket.
- You can now access the website from the CloudFront distribution domain.
- Clean up AWS resources by running the below command from project root directory
cdk destroy
This setup lets all client requests ending with /api/*
to be sent to API Gateway origin, while other requests are served from the S3 bucket hosting the SPA, which is the default behavior. This deployment lets you host SPA assets in the S3 bucket and essentially use CloudFront’s domain for accessing both the front end application and back end APIs from the client browser. In addition to the performance improvement from eliminating preflight requests, your workload also benefits from improved latency with CloudFront’s points of presence (POP’s).
However, there is a trade off to consider with this pattern. For example, a setup where all of the requests from the front end are fronted by a single CloudFront distribution, but the API gateway has other consumers not originating from CloudFront where you want to have different web application firewall rules. In this case, you need a separate web application firewall enabled for CloudFront and API gateway, which may have overlapping firewall rules. This would increase the cost depending on how many firewall rules are evaluated multiple times for the same requests.
Conclusion
In this post, we learned how to use Amazon CloudFront to serve dynamic content from API Gateway and benefit from an improved performance by eliminating preflight requests. The architecture presented in this post is ideal for SPA’s served through CloudFront distribution and backed by API Gateway or any other proprietary back end that uses RESTful APIs.