Πειραματίζομαι με διάφορες τεχνικές ώστε να βελτιώσω τις επιδόσεις του eordaia.info.
Πρώτα από όλα χρησιμοποιώ το srcset attribute του <img>, για να εμφανίζει την έκδοση της εικόνας που ταιριάζει περισσότερο με την οθόνη του χρήστη. Κρατάω 4 μεγέθη για κάθε εικόνα.
Στο srcset του λες ποια εικόνα θα εμφανίσει, αναλόγως πόσο χώρο θα πιάσει αυτή στην οθόνη. Για παράδειγμα
<img srcset="smallImage.jpg 500w,
mediumImage.jpg 1000w,
largeImage.jpg 1500w
originalImage.jpg 2000w"
src="mediumImage.jpg"
sizes="(min-width: 940px) 66vw, 100vw">
Έτσι π.χ. μέχρι το μέγεθος της εικόνας να είναι στα 500w, θα εμφανίσει το smallImage.jpg. Το w είναι στην ουσία τα φυσικά pixels της οθόνης. Αν μια οθόνη είναι retina και έχει μεγαλύτερη πυκνότητα pixels, τότε για 2x τα 250 pixels θα αντιστοιχούν σε 500w.
Μπορεί να χρησιμοποιηθεί και το attribute sizes για μεγαλύτερο έλεγχο. π.χ στον παραπάνω κώδικα αυτό που υπάρχει στο sizes, λέει ότι πάνω από τα 940 pixels (μέγεθος οθόνης), η εικόνα θα εμφανίζεται στο 66% του viewport (δηλαδή σε μεγαλύτερο οθόνη η εικόνα μπορεί να πιάνει λιγότερο χώρο). Ενώ κάτω από τα 940 pixels η εικόνα θα πιάνει όλο το μήκος του viewport. Αφού θα μικρύνει το μέγεθος της φωτογραφίας θα εμφανίσει και την ανάλογη το srcset.
Το άλλο θέμα που είχα να αντιμετωπίσω ήταν το lazy loading. Δηλαδή να μην χρειάζεται να φορτωθούν ολόκληρες οι εικόνες για να εμφανιστεί το υπόλοιπο περιεχόμενο της σελίδας.
Στην αρχή δοκίμασα ένα component που έχει το bootstrap-vue. Το <b-img-lazy>. Δεν μου άρεσε όμως. Στο production, όποτε θέλει φορτώνει μια αρχική εικόνα πολύ χαμηλής ανάλυσης που θέλω (για να δείξει αυτή μέχρι να φορτώσει η κανονική) και όποτε θέλει όχι.
Τελικά έφτιαξα ένα δικό μου vue component για να κάνει την δουλειά. Η λογική του είναι ότι φορτώνω 2 img elements. Το ένα είναι το βασικό. Αυτό που βλέπει ο χρήστης. Το άλλο είναι το κρυφό, εκεί που φορτώνεται η εικόνα μεγάλης ανάλυσης. Μέχρι να φορτώσει η μεγάλη εικόνα, στο βασικό img δείχνει μια εικόνα χαμηλής ανάλυσης. Αφού φορτώσει αντικαθιστώ το src της μικρής εικόνας με το src της μεγάλης.
Έτσι ο χρήστης βλέπει ένα εφέ στο οποίο στην αρχή η εικόνα είναι πολύ θολή και σιγά – σιγά καθαρίζει. Δίνει την ψευδαίσθηση ότι φορτώνει σταδιακά.
Το πρόβλημα με το lazy loading (τουλάχιστον όπως το έκανα προς το παρόν) είναι ότι ο browser δεν ξέρει (μέχρι να φορτώσει η εικόνα χαμηλής ανάλυσης), πόσο χώρο θα πιάσει η εικόνα στο συγκεκριμένο σημείο της σελίδας. Έτσι βλέπεις το κείμενο να “χοροπηδάει” μόλις φορτώσουν οι εικόνες.
Αυτό είναι και το θέμα που θέλω να λύσω. Ή θα πρέπει να ξέρω από πριν το μέγεθος της εικόνας (που προς το παρόν δεν το σώζω στην βάση) ή θα πρέπει ίσως να σώζω την εικόνα και σε ένα ακόμη μικρότερο μέγεθος και ποιότητα (για να φορτώνει όσο γίνεται πιο γρήγορα).
Θα δοκιμάσω το δεύτερο και βλέπουμε πως θα πάει.
Ο κώδικας του vue component είναι ο παρακάτω.
<template>
<div>
<img :src="imageDisplayed" width="100%" height="auto"
:title="photo.title" :alt="photo.title">
<img :srcset="photo.srcset"
:src="photo.fallback"
:sizes="photo.sizes"
ref="image" class="preloadedImage"
@load="imageUploaded">
</div>
</template>
<script>
export default {
data() {
return {
imageDisplayed: ''
}
},
props: {
photo: {
required: true,
type: Object
}
},
created() {
this.imageDisplayed = this.photo.preload
},
methods: {
imageUploaded() {
this.imageDisplayed = this.$refs.image.currentSrc
}
}
}
</script>
<style scoped>
.preloadedImage {
display: none;
}
</style>
Στο photo που δέχεται στα props το component, στέλνω κάτι τέτοιο (από laravel component, το οποίο καλεί το vue component)
[
'id' => $this->photo->id,
'preload' => $this->photo->smallPhotoUrl,
'fallback' => $this->photo->mediumPhotoUrl,
'title' => $photo->label ?? $this->title,
'srcset' => [
$this->photo->smallPhotoUrl . ' 150w',
$this->photo->mediumPhotoUrl . ' 1000w',
$this->photo->largePhotoUrl . ' 1500w',
$this->photo->photoUrl . ' 2000w'
],
'sizes' => '(min-width: 940px) 66vw, 100vw'
]
Έπεσα σε ένα θεματάκι, σε JavaScript, που δεν περίμενα να δουλεύει με αυτόν τον τρόπο.
Το πρόβλημα ήταν απλό. Ήθελα να προσθέσω κλώνο ενός object (που έχει κάποια έτοιμα properties), μέσα σε ένα array. Για παράδειγμα:
photos.push(emptyPhoto))
Τελικά όμως έτσι δεν αντιγράφεται απλά ένας κλώνος του emptyPhoto μέσα στο photos. Αλλά το photos θα έχει τώρα reference στο emptyPhoto. Δηλαδή τα περιεχόμενα του emptyPhoto θα αλλάζουν με ότι βάζουμε π.χ. στο photos[0].
Για να λειτουργήσει όπως θέλω, κάνω τελικά αυτό:
photos.push(Object.assign({}, emptyPhoto))
Καταρχήν προτείνεται χωρίς δεύτερη σκέψη το debugbar, που είναι ένα φοβερό debug εργαλείο. Πέρα του ότι μπορείς να πετάς ότι μηνύματα ή data θες (χωρίς να παιδεύεσαι με dd και άλλα παρόμοια που διακόπτουν την ροή της εκτέλεσης του κώδικα), σου δίνει και μια λεπτομερή εικόνα για τι στο διάολο κάνεις με την database σου. Ποια ακριβώς sql queries τρέχουν, πόση μνήμη χρησιμοποιείται, πόσα μοντέλα φορτώνονται κτλ
Η εγκατάσταση του debugbar είναι πανεύκολη, όπως εγκαθιστάς κάθε πακέτο με τον composer. Άμεσα θα σου εμφανίσει (όταν είσαι σε dev περιβάλλον και τρέχεις την εφαρμογή σου), μία μπάρα με όλα τα ενδιαφέροντα που προσφέρει το εργαλείο.
Το debugbar, λοιπόν, θα μας δείξει τα προβλήματα που υπάρχουν στην εφαρμογή μας, με τα queries που κάνουμε. Πιο χαρακτηριστικό το N+1 πρόβλημα.
Ας πούμε ότι έχουμε ένα model με τα posts μας και αυτά έχουν relation με tags. Οπότε θέλοντας να εμφανίσουμε μία λίστα με όλα τα posts και τις ετικέτες που ανήκουν σε καθένα από αυτά, θα κάνουμε κάτι τέτοιο στον controller.
$posts = Post::all();
Στην συνέχεια μέσα στο view μας θα εμφανίσουμε τα tags του κάθε post, κάπως έτσι:
@foreach ($post->tags()->get() as $tag)
{{ $tag->name }}
@endforeach
Όλα ωραία. Εμφανίζεται όπως θέλουμε το αποτέλεσμα, αλλά τι συμβαίνει από κάτω; Τι sql queries θα τρέξουν; Καταρχήν ένα select που θα πάρει όλα τα posts.
select * from posts;
Στην συνέχεια για κάθε post θα κάνει ένα ξεχωριστό select για να πάρει όλα τα tags που ανήκουν σε αυτό, κάπως έτσι.
select * from tags where id = ?
select * from tags where id = ?
select * from tags where id = ?
select * from tags where id = ?
select * from tags where id = ?
.
.
.
Ν φορές
Άρα το σύνολο των queries θα είναι Ν+1. Καταλαβαίνουμε ότι σε μεγάλες βάσεις, αυτό δεν είναι αποδεχτό. Τι κάνουμε λοιπόν;
Κάνουμε eager loading. Δηλαδή αντί να τρέχει ένα ξεχωριστό query κάθε φορά που ζητάμε τα tags ενός post, τα προσθέτει μέσα στο post όλα από την αρχή. Για να το πετύχουμε αυτό, κάνουμε κάτι τέτοιο:
$posts = Post::with('tags')->get();
Αντίστοιχα στο view:
@foreach ($post->tags as $tag)
{{ $tag->name }}
@endforeach
Με τον τρόπο αυτό έχουν γίνει μόνο 2 queries!
select * from posts;
select * from tags where id in (1, 2, 3, 4, 5, ...);
Στο eordaia.info που δουλεύω τελευταία, διόρθωσα έτσι τα queries μου, αλλά μου έτυχε ένα περίεργο bug. Ξενύχτησα χθες για να καταλάβω τι παίζει και το βρήκα σήμερα. Ενώ γενικά τα περισσότερα posts εμφανίζονταν σωστά και γίνονταν load και όλα τα relations για κάθε ένα από αυτά, σε κάποια posts δεν δούλευε το eager loading. Δεν μπορούσε να τραβήξει τα data από τα relations.
Το δύσκολο ήταν να καταλάβω τι περίεργο έχουν κάποια posts σε σχέση με τα άλλα που δούλευαν. Τελικά βρήκα τι έφταιγε και αυτό ήταν το ότι χρησιμοποιώ uuid’s για τα id’s. Οπότε για να τραβήξει τα tags π.χ. έκανε κάτι τέτοιο:
select * from tags where id in (3535, 2, 33435, 2123, 54756, ...);
Δηλαδή μέσα στην παρένθεση αντί να ψάχνει για uuid’s τα μετέτρεπε σε integer. Έτσι, κάποιες μετατροπές ξέφευγαν από το όριο των integers και έψαχνε άλλα αντί άλλων.
Η λύση είναι τελικά να βάλεις στο model (π.χ. Post.php) την συγκεκριμένη γραμμή:
protected $keyType = 'string';
Με τον τρόπο αυτό τα uuid’s μετατρέπονται σε string κι έτσι δουλεύουν όλα σωστά.