开发者控制台
感谢您的访问。此页面目前仅提供英语版本。我们正在开发中文版本。谢谢您的理解。

Achieving App Quality Through Performance and Stability

Moses Roth Mar 13, 2025
Share:
Best practices
Blog_Header_Post_Img

As you build your app for Amazon Fire devices, getting your features just right is definitely important. Building a great app also means creating a smooth and reliable experience that your users will enjoy. Crashes, lags, and janky UI interactions will lead to bad reviews and even uninstalls. No developer wants that.

So addressing performance issues and keeping your app stable are incredibly important parts of your app developer mission. They’re key to keeping users happy and ensuring your app succeeds. Thankfully, with tools from the Amazon Developer Console like the App Health Insights dashboard, it’s easier to spot problems and make the right fixes.

This guide will walk you through how to understand the dashboard and what to look for. You’ll find many tips to improve your app’s performance and stability. Are you ready to make sure your app shines on every Fire device? Then let’s get started!

Introducing the App Health Insights dashboard

The App Health Insights dashboard is a powerful tool to help you understand how your app is performing. It provides key metrics and actionable insights so you can focus on fixing what matters most.

To access it, you’ll need a live app. To get started:

  1. Go to the Developer Console dashboard
  2. Click Apps & Services in the top navigation bar
  3. Open the Action menu on your app
  4. Click App Health Insights

Overview of the dashboard

The dashboard is divided into two main sections: the performance dashboard and the stability dashboard. Each section focuses on a specific aspect of your app’s health.

  • Performance dashboard: This area tracks metrics like app launch times and frame rates, helping you understand how smoothly your app runs and how quickly it responds to user interactions.
  • Stability dashboard: Here, you’ll find data on crash rates, Application Not Responding (ANR) errors, and other stability-related issues that could frustrate your users.

With this data, you don’t have to guess what’s going wrong—you can see it clearly and address it right away.
 

Why the dashboard is helpful

For developers, time is precious. The dashboard saves you from digging through endless logs or waiting for user complaints to roll in. Instead, it highlights the most critical problems in your app, letting you prioritize fixes that will have the biggest impact on your users’ experience. By tackling these issues early, you can reduce negative reviews and improve retention.

Performance dashboard metrics

The performance dashboard helps you measure your app’s efficiency. By focusing on a few key metrics, you can identify and address bottlenecks that impact the user experience.

App latency metrics

App latency metrics track two key aspects of how your app launches: app launch time and ready-to-use time.

Amazon developer security profiles menu image

Both metrics are critical because they influence the user’s perception of speed and responsiveness. Exceeding these benchmarks can make the app feel slow and lead to user frustration.

To reduce app launch time and ready-to-use time, here are some things you can do:

  • Implement lazy loading: Only load resources when they are actually required. Disable auto-initialization for non-critical libraries. Use Android Studio Profiler to identify which resources take the longest to load. This technique can significantly reduce the initial data and resource overhead during app startup.
  • Optimize initial UI: Start with a simple, static UI that can be drawn quickly. Use placeholders for views instead of complex layouts, and defer intensive animations and drawing operations until after you've called reportFullyDrawn(). This approach helps users see something immediately while the rest of the app continues to load in the background.

Improve data management efficiency: Use file system operations instead of databases for initial data loading, which can help speed up application launch significantly. Consider serializing relevant data directly to the file system for faster retrieval. The following example implements fast serialization and deserialization of the data to be presented to the user initially:

Copied to clipboard
public void serialize(Context context, String filename, 
                      Serializable object) throws Exception {
  FileOutputStream fOut = 
    context.openFileOutput(filename, Context.MODE_PRIVATE);
  ObjectOutputStream oOut = new ObjectOutputStream(fOut);
  oOut.writeObject(object);
  oOut.close();
}

public Object deserialize(Context context, 
                          String filename) throws Exception {
  FileInputStream fIn = context.openFileInput(filename);
  ObjectInputStream oIn = new ObjectInputStream(fIn);
  Object result = oIn.readObject();
  oIn.close();

  return result;
}
Copied to clipboard
private val executor = Executors.newSingleThreadExecutor()

fun <T : Serializable> serialize(context: Context, filename: String, data: T) {
    executor.execute {
        try {
            context.openFileOutput(filename, Context.MODE_PRIVATE).use { fileOut ->
                ObjectOutputStream(fileOut).use { objOut ->
                    objOut.writeObject(data)
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

fun <T : Serializable> deserialize(context: Context, filename: String, callback: (T?) -> Unit) {
    executor.execute {
        val result: T? = try {
            context.openFileInput(filename).use { fileIn ->
                ObjectInputStream(fileIn).use { objIn ->
                    @Suppress("UNCHECKED_CAST")
                    objIn.readObject() as? T
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
        callback(result)
    }
}

As an alternative to the traditional serialization method shown above, consider the use of Flatbuffers for a flexible and memory efficient approach.

  • Use background threads: Leverage background threads to perform tasks while your app is loading. This can help reduce the time users have to wait for your app to become usable by moving intensive operations off the main thread.
  • Measure and iterate: Use Android's performance tools to capture macro-benchmark metrics. Calculate average startup times after multiple measurements to understand your app's performance and identify areas for improvement.

For more details and code examples on these recommendations, check out How to measure and improve app startup times in Fire OS.

Foreground Low Memory Killer Events (LME) for memory usage

The Foreground Low Memory Killer Events (LME) metric tracks the average daily instances of foreground low memory events that kill the app on the device. A foreground LME occurs when the system is critically short on memory, even after terminating all non-persistent background apps or services. This metric helps you understand how frequently your app is experiencing severe memory constraints that result in forcibly closing the app.

Amazon developer security profiles menu image

Minimizing these events depends on healthy memory usage for your app. Of course, this depends on your app’s complexity. However, as a basic benchmark, strive to limit foreground memory consumption to less than 1000 MB for gaming apps and less than 600 MB for non-gaming apps. Excessive memory usage that triggers frequent LMEs can lead to app instability or crashes, particularly on devices with limited resources.

For a comprehensive guide on managing your app's memory, see the Android Developers page on reducing app memory usage. The following strategies capture key takeaways from that resource, helping you reduce memory usage and improve your app's performance:

  • Implement memory monitoring: Use the Memory Profiler in Android Studio to track how your app allocates and uses memory. This tool helps you identify memory-intensive code segments and understand your app's memory consumption patterns over time.
  • Optimize memory callbacks: Implement the ComponentCallbacks2 interface to proactively manage memory usage. This allows your app to respond intelligently when the system is running low on memory. Here's an example:

Copied to clipboard
public class MainActivity extends AppCompatActivity
  implements ComponentCallbacks2 {

  // Other activity code.

  /**
   * Release memory when the UI becomes hidden or when system
   * resources become low.
   * @param level the memory-related event that is raised.
   */
  public void onTrimMemory(int level) {

    if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
      // Release memory related to UI elements, such as bitmap caches.
    }
    if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
      // Release memory related to background processing, such as by
      // closing a database connection.
    }
  }
}
Copied to clipboard
import android.app.Activity
import android.content.ComponentCallbacks2
import android.content.res.Configuration
import android.os.Bundle
import android.util.Log

class MainActivity : Activity(), ComponentCallbacks2 {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        registerComponentCallbacks(this)
    }

    override fun onTrimMemory(level: Int) {
        when {
            level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                // Release memory related to UI elements, such as bitmap caches
                Log.d("MemoryTrim", "UI is hidden. Freeing up UI-related resources.")
            }
            level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> {
                // Release memory related to background processing, such as closing a database connection
                Log.d("MemoryTrim", "App in background. Releasing background resources.")
            }
        }
    }

    override fun onLowMemory() {
        // Handle extreme low-memory situations
        Log.w("MemoryTrim", "System is running low on memory!")
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        // Required override but not used for memory management
    }

    override fun onDestroy() {
        super.onDestroy()
        unregisterComponentCallbacks(this)
    }
}

  • Memory-efficient coding: Avoid creating unnecessary objects in performance-critical areas. Use object pools to reduce allocation overhead, and be especially careful about object creation in inner loops or drawing methods.

  • Resource and library management: Remove unnecessary libraries and reduce your APK size. Use code-shrinking tools to eliminate unused code and optimize your app's memory footprint.

  • Service management: Use services sparingly and stop them immediately after they complete their tasks. Avoid leaving unnecessary services running in the background, which can consume memory.

  • Proactive memory checking: Check device memory status before performing memory-intensive operations. This helps prevent your app from running operations when system resources are constrained:

     

Copied to clipboard
public void doSomethingMemoryIntensive() {

  // Before doing something that requires a lot of memory,
  // check whether the device is in a low memory state.
  ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();
    if (!memoryInfo.lowMemory) {
      // Do memory intensive work.
  }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
  ActivityManager activityManager =
    (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
  ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
  activityManager.getMemoryInfo(memoryInfo);
  return memoryInfo;
}
Copied to clipboard
import android.content.Context

fun Context.isMemoryLow(): Boolean {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    val memoryInfo = ActivityManager.MemoryInfo()
    activityManager.getMemoryInfo(memoryInfo)
    return memoryInfo.lowMemory
}

fun doSomethingMemoryIntensive(context: Context) {
    if (!context.isMemoryLow()) {
        // Perform memory-intensive operation
    }
}

  • Dependency injection: Consider using dependency injection frameworks, which help manage object creation and lifecycle more efficiently. These frameworks can reduce memory overhead by providing a more controlled way of creating and managing object dependencies.
     
Fluidity for frame drop rate

Fluidity measures the smoothness of animations and transitions by tracking frame drop rates. This metric highlights how often frames are skipped during rendering, which can make your app feel choppy and less responsive.

The Test Criteria for Amazon Appstore Apps states:

Apps should sustain a high frame rate without dropping below 25 fps for an extended period of time.… The recommended average frame rate is 55-60 fps, especially for apps that involve fast-moving graphic objects.

For a smooth experience, apps should aim for a frame drop rate of less than 5%. Rates exceeding 10% can significantly degrade the user experience, making the app appear laggy and unpolished.

To improve fluidity, simplify complex animations and reduce the number of objects rendering simultaneously. Optimize rendering tasks by ensuring efficient GPU usage and minimizing unnecessary computations during transitions. Tools like Jank Detection in the Android Studio Performance Profiler and Profile GPU Rendering tool can help pinpoint and resolve rendering issues effectively.


Stability dashboard metrics

The stability dashboard helps you identify and resolve issues that could cause your app to crash, hang, or otherwise frustrate users. By monitoring a few key metrics, you can proactively improve your app’s reliability and user experience.

Amazon developer security profiles menu image

Crash rates

Crash rate measures how often your app crashes relative to the number of user sessions. This metric is critical because frequent crashes can quickly lead to poor reviews and uninstalls.

For a healthy app, crash rates should remain below 1%. Apps with crash rates exceeding this benchmark risk alienating users and eroding trust. To reduce crash rates, developers should implement the following strategies recommended to Android developers:

  • Conduct thorough null checking to prevent instances of NullPointerException, which are common sources of app crashes.

  • Annotate method parameters and return values with @Nullable and @NonNull to catch potential null-related issues at compile time.

  • Carefully handle exceptions with specific catch blocks instead of generic exception handling.

  • Perform comprehensive testing across different Fire tablet models.

  • Implement proper logging to capture detailed error information.

By systematically addressing these areas, developers can significantly reduce crash rates and improve overall app stability on Fire tablets.

Application Not Responding (ANR) errors

ANRs occur when your app’s main thread is unresponsive for too long, causing the system to notify users that the app isn’t working. This often leads to users force-quitting your app.

Healthy apps should aim to have an ANR rate of less than 0.47%. Higher rates indicate your app may have performance bottlenecks or blocking operations on the main thread.

To address ANRs, implement these recommended fixes:

Copied to clipboard
// Bad example: Performing time-consuming task on the main thread
public void onClick(View v) {
  // This will block the UI and potentially cause an ANR
  Bitmap bitmap = processBitMap("image.png");
  imageView.setImageBitmap(bitmap);
}

// Good example: Moving time-consuming task to a background thread
public void onClick(View v) {
  new Thread(new Runnable() {
    public void run() {
      // A potentially time-consuming task performed off the main thread
      final Bitmap bitmap = processBitMap("image.png");


      // Use post() to update UI safely from the main thread
      imageView.post(new Runnable() {
        public void run() {
          imageView.setImageBitmap(bitmap);
        }
      });
    }
  }).start();
}
Copied to clipboard
import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.ImageView
import java.util.concurrent.Executors

class MyActivity {

    private val executor = Executors.newSingleThreadExecutor()
    private val mainHandler = Handler(Looper.getMainLooper())

    fun onClick(view: View, imageView: ImageView) {
        executor.execute {
            // Perform time-consuming task in background thread
            val bitmap = processBitmap("image.png")

            // Update UI safely on the main thread
            mainHandler.post {
                imageView.setImageBitmap(bitmap)
            }
        }
    }

    private fun processBitmap(filename: String): Bitmap {
        // Simulate bitmap processing (e.g., decoding an image)
        Thread.sleep(2000) // Simulate delay
        return Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
    }
}

  • Avoid synchronous I/O operations on the main thread.
  • Break long calculations into smaller, manageable chunks.
  • Prevent lock contention between main and worker threads.
  • Use StrictMode during development to detect accidental main thread I/O.
  • For broadcast receivers, use IntentService or goAsync() for long-running operations.
  • Regularly profile your app using tools like the Android Studio CPU Profiler to identify performance bottlenecks.
     
Other considerations

Beyond the performance and stability dashboards, there are additional factors to keep in mind to ensure your app delivers the best experience possible:

  • Battery efficiency: Minimize battery drain by optimizing background processes and reducing resource-intensive tasks.
  • Network usage: Limit excessive data usage by caching data where appropriate and optimizing API calls.
  • Device compatibility: Test your app across various Fire tablet models to ensure consistent performance and functionality.
  • Usability: Focus on intuitive designs and straightforward navigation to enhance user satisfaction.
  • Content quality: Provide high-quality content that is relevant, engaging, and optimized for Fire tablet displays.
  • Accessibility: Incorporate accessibility features like screen reader support, high-contrast modes, and touch-friendly interfaces to make your app inclusive.

Conclusion

Creating a high-quality app for Fire devices means paying attention to performance and stability. With the App Health Insights dashboard, developers can focus on key metrics to ensure they’re delivering an app with a smooth and reliable experience that keeps users coming back.

Interested in diving deeper? The following resources will help you on your journey toward continuous improvement of your app quality:

Media assets

Related articles

Sign up for our newsletter

Stay up to date with the latest Amazon Developer news, industry trends and blog posts.