Applications built with a Java back-end, a relational database (such as Oracle or MySQL), and a JavaScript-based front-end form a common and widely adopted web application stack. No one wants to wait long to get the results they want from a web application. Ensuring optimal performance is crucial and should be integrated into every phase of planning and development. This article will present actionable practices to boost your application's performance and maintain fast response times. These strategies will cover optimizations at the database, Java, and JavaScript layers.
1. Database
One of the most common reasons for application slowness is access to external resources such as databases. That’s why you need to adopt certain behaviors and follow rules to make your application more efficient, such as:
Pooling: Database connection pooling is a technique that maintains a pool of open, ready-to-use database connections. Establishing a database connection is an expensive operation, involving network setup, authentication, and authorization checks, all of which consume significant time. Connection pooling minimizes the overhead of repeatedly creating new connections by allowing applications to reuse existing, active connections from the pool. This reuse significantly reduces the time required to execute database operations, leading to improved application performance and responsiveness.
Minimize Database Access: Optimize database interactions by reducing the frequency and granularity of requests. Instead of performing numerous small operations, consider batching related actions into a single database call. Furthermore, strategically caching frequently accessed data within your application's memory structures can significantly reduce the need to query the database repeatedly, leading to faster data retrieval and overall performance gains. While in-memory processing can be quicker for certain tasks, carefully evaluate data consistency requirements and potential memory constraints.
Minimize Expensive Database Operations: Reduce the use of inherently costly database operations. For example, avoid performing extensive string manipulations directly within database queries, as these can be less efficient than handling them within the application layer. Consider alternative data types and processing strategies to minimize the database's workload and improve query performance.
Optimize your SQL queries: To improve performance, reduce resource usage, and deliver faster response times. This is especially crucial when working with large datasets or complex joins. Let’s take an example of a correlated subquery and how to write it efficiently using Oracle SQL join.
Let's take the famous example of the 'customers' database. We want to find all customers who have made at least one purchase over 5,000. Using a correlated subquery, we get:
SELECT c.customer_id, c.customer_name
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
AND o.order_total > 5000
);
- This query is less performant because it runs once for each row in the customers table.
- On large datasets, this approach leads to numerous redundant executions, resulting in significant performance degradation.
- While indexing orders.customer_id or orders.order_total may offer some improvement, it remains a suboptimal solution.
You can make this query more efficient if you use join with DISTINCT as follows:
SELECT DISTINCT c.customer_id, c.customer_name
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE o.order_total > 5000;
The optimization advantages of this query over the first one are:
- Transforms the correlated subquery into a set-based join, allowing Oracle to leverage its optimized join algorithms for improved performance.
- The DISTINCT clause ensures that each customer is only returned once, even if they have multiple orders that meet the criteria.
- With indexes on orders.customer_id, and orders.order_total, Oracle can efficiently filter and join the tables, minimizing I/O operations and maximizing query speed.
Utilities to Boost Queries Performance
Database management systems like Oracle or MySQL offer utilities that help you understand the execution of your queries. So, you can analyze the actual performance of a query, debug performance issues, or tune queries.
In Oracle, two tools, EXPLAIN PLAN and AUTOTRACE, are defined. The EXPLAIN PLAN reveals Oracle's execution plan for your SQL statement, including tables and indexes used, join methods, and operation order. It provides an estimation of how the Oracle optimizer will execute a SQL query.
AUTOTRACE is a utility integrated into tools like SQL*Plus or SQL Developer. It provides the execution plan and details about query execution, such as statistics like CPU time, row count, and other performance metrics.
In MySQL, there are tools like EXPLAIN and ANALYZE. EXPLAIN provides an estimated execution plan of an SQL statement without running it. On the other hand, ANALYZE EXPLAIN executes your query and provides a detailed execution plan.
These tools help to gain insight into query performance and optimization opportunities.
Tips for Query Optimization
Optimizing database queries is crucial for application performance. Here are several key strategies to consider:
• Create indexes on frequently queried/joined columns, especially those in WHERE, JOIN, and ORDER BY clauses.
• Avoid applying functions to indexed columns in WHERE clauses (e.g., FUNCTION(column)) to preserve index usability.
• Avoid Full Table Scans. Only if they are necessary.
• Improve query readability and performance by using WITH clauses (CTEs) for clarity and reuse and converting correlated subqueries to joins.
• Use bind variables (parameters) to facilitate plan caching and mitigate hard parsing.
• Gather statistics to maintain current table and index statistics, enabling the optimizer to generate efficient execution plans based on accurate data.
2. Java Code
To enhance the performance of your Java code in a Java web application, consider following these best practices:
• Avoid creating unnecessary objects. Reuse objects where possible to reduce memory usage and garbage collection overhead.
• Use StringBuilder instead of string concatenation, especially within loops, to improve performance and reduce object creation.
• Minimize session data storage. Avoid storing large volumes of data in the session to prevent excessive memory usage.
• Set short session timeouts and invalidate sessions when they are no longer needed to free up resources promptly.
• Use static objects sparingly. Static objects persist for the lifetime of the application and can lead to memory leaks if not managed carefully.
• Always release closable resources, such as database connections, in finally blocks or use try-with-resources to ensure proper cleanup.
• Implement concurrent threads or tasks to take advantage of multi-core processors and improve application responsiveness.
• Activate caching to store frequently accessed data retrieved from the database. Subsequent requests can then be served directly from this cache, avoiding the overhead of querying the primary storage unless the data is not present or has expired. This significantly improves response times and reduces database load.
• Use asynchronous processing for long-running requests, especially when the server delegates tasks to third-party services, to prevent blocking and improve scalability.
• Avoid quadratic (O(N²)) or higher time complexity algorithms in performance-critical sections. Replace nested loops with more efficient strategies like indexing or divide-and-conquer approaches to improve scalability.
• Use efficient data structures such as HashMap, Set, or TreeMap to optimize performance. Use Java Streams or parallel streams carefully—they can enhance code clarity and concurrency, but don’t reduce the underlying algorithmic complexity.
• Reduce Server Load: To reduce server load, performance-intensive operations such as sorting and filtering should ideally be performed within the SQL query or client-side using JavaScript, depending on the specific use case and data volume.
3. JavaScript Code
To improve the performance of your JavaScript code in a Java web application, consider following these best practices:
Load Scripts Efficiently
• Break large scripts into smaller, manageable modules and load only what is needed for each page.
• Avoid blocking the main thread to keep your application responsive. You can also use web workers that allow you to run JavaScript code in the background.
• A page should be displayed quickly. Any slow data blocking the page display must be loaded asynchronously using mechanisms such as async or Ajax.
• Defer loading non-essential content like images and videos until they are about to be visible using the loading="lazy" HTML attribute on <img> elements and JavaScript Intersection Observers for more advanced control.
Use Efficient Data Structures and Algorithms
• Select the right data structures (e.g. Set, Map, arrays) for your use case and avoid inefficient algorithms (e.g. O(N²) operations) on large datasets.
• Cache expensive computations when possible.
Leverage Appropriate Caching Mechanisms
• Use localStorage or sessionStorage to persist user preferences, filters, or data between page loads.
• Use in-memory caching (e.g., variables or data-* HTML attributes) when data should be temporary and cleared on page refresh.
• Refer to [my JavaScript caching tutorial] for detailed use cases and examples.
Debounce or Throttle High-Frequency Events
• Apply debounce or throttle techniques to high-frequency events such as scroll, resize, or keyup. These techniques help prevent unnecessary function executions, reducing performance overhead and optimizing your application's responsiveness.
Minimize Direct DOM Manipulation. Access and update the DOM efficiently by batching changes or minimizing reflows/redraws. Also, avoid excessive use of innerHTML and appendChild, and modifying DOM elements in loops without optimization. Let's delve into details.
Manipulating the DOM (Document Object Model) involves directly changing a webpage's content using JavaScript, such as adding elements, modifying content, or applying styles. However, frequent and direct DOM manipulation can be slow and inefficient, particularly for complex or dynamic interfaces. To improve performance, consider the following best practices:
Batch Changes. Instead of making individual DOM changes in a loop, group them and apply the updates in a single batch. This technique reduces the number of times the browser needs to recalculate the page layout, resulting in improved performance. To further illustrate this, consider the following example:
// Before: individual updates in a loop
for (let i = 0; i < 100; i++) {
const element = document.createElement('div');
document.body.appendChild(element);
}
// After: batched updates
const fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
const element = document.createElement('div');
fragment.appendChild(element);
}
document.body.appendChild(fragment);
Minimize Reflows/Repaints. Changing layout-related styles, such as size, position, or fonts, triggers a reflow or repaint, which can be costly. This process involves recalculating and redrawing parts of the page, impacting performance.
When accessing and modifying layout-related properties, such as offsetHeight or clientWidth, do so in separate blocks of code to avoid forced synchronous reflows. Avoid interleaving reads and writes to prevent unnecessary reflows. Here's an illustrative example of this topic:
Avoid Excessive Reparsing and Recalculations. Here are some points to consider to improve your application's performance:
• Setting innerHTML replaces the entire content of an element, forcing a reparse of the HTML. Minimize the use of innerHTML and instead use targeted updates.
• Repeatedly calling appendChild in a loop triggers multiple layout recalculations. Instead, use a DocumentFragment to batch updates.
• Avoid setting styles in a loop. Instead, use classes or apply all styles at once to minimize recalculations.
The following example is for using cssText to update multiple styles at once instead of updating individual styles:
// Modify styles: bad practice
element.style.height = '100px';
element.style.width = '200px';
// Modify styles: good practice
element.style.cssText = "width: 100px; height: 50px;";
// Bad practice: interleaved reads and writes
const height = element.offsetHeight;
element.style.height = '100px';
const width = element.offsetWidth;
// Good practice: separate blocks
const height = element.offsetHeight;
const width = element.offsetWidth;
Minify and Bundle JavaScript Files
• Minify JS code and bundle multiple scripts to reduce file size and HTTP requests. Use tools like Webpack to minify code, bundle scripts, and improve page load times.
Avoid Memory Leaks
• To prevent memory leaks, especially during page transitions or single-page app (SPA) routing, it's crucial to clean up resources that are no longer needed by removing event listeners, clearing timeouts and intervals, and dereferencing DOM elements. Let's note that in the JavaScript environment in the SPA persists across these page transitions.
Profile Using Browser DevTools
Unlock the potential of your browser's DevTools, such as Chrome DevTools or Firefox Developer Tools, to gain a deeper understanding of your web application's performance and resource utilization. With these built-in tools, you can profile your web application:
• Memory: Analyze consumption over time to identify leaks and optimize usage.
• Rendering: Inspect performance details to improve page rendering speed.
• Script: Measure JavaScript execution to optimize code performance.
• Network: Check load times of your files and requests for faster loading.