Optimizing Laravel starts within

Last week, I posted about a learning experience I thought would help any Laravel developer. This time, I’ll be talking about the importance of optimizing your app and how it can unexpectedly go wrong.

If you’re unfamiliar with the deployment process of a Laravel app, I recommend reading this page from the official Laravel docs.

In my experience, caching your views, configurations, and such is a great start, but not the only thing you should do. In fact, I’ve found that most performance issues cannot be solved by simple caching. More often than not, it is our code that needs some refactoring and rethinking.

Unnecessary appending and eager loading

I have an app that relies heavily on Vue components so I loaded a lot (and I mean a lot) of relationships so I could easily access them from the JSON object created for the view. The problem with this is that some models are used more often than others. That is, if your model is loaded on most pages (even if not all the loaded relationships are used), then your app will eventually suffer from overloading.

It’s hard to imagine as it is, so let me explain with an example. As you may know, eager loading alleviates the N + 1 problem; i.e., it may reduce N + 1 queries to only 2 queries. However, if you do this too many times unnecessarily, it will not be a good thing after all. Consider a Patient model that belongs to a Physician model that, in its turn, has many WorkingHours (time ranges, this could be implemented in many different ways) and Absences that are used in the scheduling section.

On the one hand, most of the patient views will include the physician information as well. On the other hand, and more often than not, you’ll be loading the physician information to schedule appointments. So, you’ll be tempted to do something like this:

use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Patient extends Model {

    // ...

    protected $with = ['physician'];

    public function physician(): BelongsTo {
        return $this->belongsTo(Physician::class);
    }

    // ...

}
use Illuminate\Database\Eloquent\Relations\HasMany;

class Physician extends Model {

    // ...

    protected $with = ['working_hours', 'absences'];

    public function working_hours(): HasMany {
        return $this->hasMany(WorkingHour::class);
    }

    public function absences(): HasMany {
        return $this->hasMany(Absence::class);
    }

    // ...

}
class PatientController extends Controller {

    // ...

    public function show(Patient $patient){
        return view('patients.show', compact('patient'));
    }

    // ...

}
<div id="patient-info">
    <patient_info
        v-model="{{ $patient }}"
    ></patient_info>
</div>
<template>
    <h1>{{ patient.name }}</h1>
    <p>{{ patient.physician.name }}</p>
</template>

<script>
import { reactive } from 'vue'

export default {
    name: "patient_info",
    props: {
        modelValue: Object
    },
    setup(props){
        const patient = reactive(props.modelValue)
        return {
            patient
        }
    }
}
</script>

This will be an issue in the long run. Yes, you can assume that you’ll be using the information like this. However, there is no real need to eager load this way. Sometimes, it’s best to do it directly on the component and sacrifice time [making sure the data is available where needed by checking every controller/view] than to always load the information.

Just in case you can’t see the issue here, let me show you what I mean. Let’s analyze what we have. Whenever we visit a patient view or search for a patient, we will have the physician information at hand. Since the physician always eager loads the scheduling information, we will also have that at hand. However, what we will need at most from the physician is their name, last name, photo, and model ID.

This is a simple example but, if we search for a patient from a 30K-row table and the search isn’t specific enough (remember user land), then we will end up with a very long array of results with unnecessary, repeated physician information for each patient. Not only that, but we don’t even care about the working hours and absences in these cases and they’re retrieved anyway. Still unconvinced? Just wait until your tables grow larger.

So, what once seemed fine, now becomes an issue. That’s what I mean when I say that optimization starts within.

You can caché every single part of your code, but if the code itself isn’t optimized, then it won’t matter as much.

Therefore, remove your unnecessary eager loading attributes from your classes and do something like this in your components instead:

<div id="patient-info">
    <patient_info
        v-model="{{ $patient }}"
        :physician="{{ $patient->physician }}"
    ></patient_info>
</div>
<template>
    <h1>{{ patient.name }}</h1>
    <p>{{ physician.name }}</p>
</template>

<script>
import { reactive } from 'vue'

export default {
    name: "patient_info",
    props: {
        modelValue: Object,
        physician: Object
    },
    setup(props){
        const patient = reactive(props.modelValue)
        const physician = reactive(props.physician)
        return {
            patient,
            physician
        }
    }
}
</script>

Or, if you really want to keep your components with one single prop (for this specific example), then you can do this in your controller:

class PatientController extends Controller {

    // ...

    public function show(Patient $patient){
        $patient->load('physician');
        return view('patients.show', compact('patient'));
    }

    // ...

}

Now that your models don’t eager load everything just for the sake of doing it and having everything at hand, your app will run faster. Just tell your app to load the models whenever you actually need them.

Need some numbers? My app went from timing out on some views with many loaded relationships to loading in under 2 seconds. Timeouts were set up for 60 seconds. I don’t know what else to say to convince you that the $with attribute is fine and helpful but you shouldn’t abuse of it.

So, what about appending? Well, it’s not as bad, it depends on your functions. You shouldn’t worry about O(1) attributes but, generally speaking, they are an analogy and you should probably be as careful as with eager loading. Also, make sure they really are O(1). You don’t want to end up using a relationship there and append it, that would almost be like eager loading it.

Leave a comment

Your email address will not be published. Required fields are marked *