Por qué tus páginas con DataTables tardan tanto en cargar

Una página que tarda mucho en cargar es una página mala.

Lo sabés vos y lo saben tus usuarios.

Si tu sitio tarda más de 5 segundos en mostrarse tus visitantes se sentirán así:

Las páginas que usan DataTables se ven bien y son muy funcionales pero pueden tardar una eternidad en desplegarse.

Más de la mitad de los usuarios que no encuentran pronto lo que buscan se van a otro sitio.

Y los que se quedan desconfían de la profesionalidad de quien las desarrolló.

¿Y quién quiere contratar a alguien que entrega trabajos de dudosa calidad?

Más vale dedicar unos minutos a aprender cómo mejorarlas.

Factores que afectan el tiempo de carga de tu página

En su forma más simple, un DataTable puede cargarse usando un arreglo JavaScript.

Basta un código como este:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="//cdn.datatables.net/1.10.24/css/jquery.dataTables.min.css">
    <title>DataTables example</title>
</head>
<body>
<h1>Behold... the power of DataTables!</h1>
<table id="theTable" class="display" style="width: 100%">
    <thead>
        <tr>
            <th>Id</th>
            <th>Name</th>
            <th>Price</th>
        </tr>
    </thead>
    <tfoot>
        <tr>
            <th>Id</th>
            <th>Name</th>
            <th>Price</th>
        </tr>
    </tfoot>
</table>
</body>
</html>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/1.10.24/js/jquery.dataTables.min.js"></script>
<script type="application/javascript">
    $(document).ready( function () {
        $('#theTable').DataTable({
            'data': [
                [1, 'Box', 10.0],
                [2, 'Chair', 20.2]
            ],
        });
    } );
</script>

Por supuesto que si toda la información tuviese que estar hardcodeada no sería muy útil la aplicación, ¿cierto?

Una versión algo más realista sería combinar este código con algo de PHP que levante información de una db, por ejemplo:

<?php

try {
    $conn = new PDO('sqlite:dt.sq3');
} catch (PDOException $exception) {
    die($exception->getMessage());
}

$sql = "SELECT * FROM products";
$st = $conn
->query($sql);

if ($st) {
$rs = $st->fetchAll(PDO::FETCH_ASSOC );
?>
<script type="application/javascript">
    $(document).ready( function () {
        $('#theTable').DataTable({
            'data': [
                <?php
                foreach ($rs as $row) {
                    ?>
                [ '<?php echo $row['id'];?>', '<?php echo $row['name'];?>', '<?php echo $row['price'];?>' ],
                    <?php
                }
                ?>
            ],
        });
    } );
</script>
<?php
} else {
    var_dump($conn->errorInfo());
    die;
}

Hasta aquí, todo perfecto.

El problema se presenta cuando la cantidad de registros es grande.

La definición de grande es un poco esquiva… podés empezar a notar problemas con 500 registros pero si estás tratando con algo como 150 000 o más definitivamente tus páginas van a tardar una eternidad en mostrarse.

Aún si tenés páginas pequeñas (Digamos, de 10 registros cada una), el tiempo total de carga no se modifica en absoluto…

¿Por qué sucede esto?

Como en cualquier otro script, esto es, a grosso modo lo que ocurre:

  1. El servidor recibe la petición
  2. Se ejecuta el PHP
  3. Se realiza la consulta a la db
  4. El servidor recibe todos los registros de la tabla
  5. Se genera el HTML y el JavaScript
  6. Se envía todo al cliente
  7. El cliente interpreta el HTML y el JavaScript
  8. Se ejecuta el JavaScript y se dibuja la tabla y todo lo demás

Como te podrás imaginar, para trabajar con los registros de la primera página no es realmente necesario tener en memoria las otras 500…

Toda esa información que no se utilizará no hace más que sobrecargar tanto al servidor (especialmente al de bases de datos) como al cliente.

Es más, si hay muchos usuarios accediendo a la vez el problema se va multiplicando, con lo cual la experiencia de uso es peor para todos 🙁

Podrías intentar atacar el problema mejorar los recursos de hardware, implementando algún tipo de caché o similar y es posible que todo eso ayudara.

Sin embargo, existe una solución mucho más al alcance de tus manos.

La solución: Procesamiento del lado del Servidor

Y aquí viene la genialidad de DataTables… ¡lo pensaron todo estos muchachos!

Este plugin admite dos modos de procesamiento: Client-Side (Es decir, todo se hace en el navegador del cliente) o Server-Side (El procesamiento se hace del lado del servidor y sólo se realiza el rendering del lado del cliente).

El modo por defecto es, por supuesto, Client-Side.

He ahí la clave del problema.

Trabajar con Server-Side es un poco más complicado, pero tampoco es terrible.

Lo más importante es comprender que, en este modo, DataTables hará una llamada Ajax cada vez que se requiera re-dibujar la información de la tabla (Al paginar, ordenar, etc…).

Para comenzar a usar este modo necesitás cambiar la configuración de tu DataTable por algo como:

$('#theTable').DataTable( {
    serverSide: true,
    ajax: '/get_data.php'
} );

Claro que, a diferencia del ejemplo en que la tabla se carga por Ajax sólo al comienzo (o cuando se refresca), esta versión de get_data.php deberá ser bastante más inteligente para poder responder a las órdenes que le dará el FrontEnd.

Qué envía el FrontEnd al BackEnd en DataTables Server-Side

La comunicación entre FrontEnd y BackEnd se realizará a través del envío de parámetros a través de la URL de la llamada (salvo que lo cambies explícitamente).

Esto implica que tu script php deberá recogerlos desde $_GET.

A modo de resumen te comento los más importantes (La lista completa la podés encontrar acá):

start: Número de registro por el que debe comenzar la paginación

length: Cantidad de registros que se espera que devuelva el servidor. Es decir, a partir del registro start, retorná length registros (Similar al LIMIT X OFFSET Y de SQL)

search: Arreglo que contiene los parámetros de la búsqueda realizada por el usuario. Esta búsqueda es global, es decir, aplica a todas aquellas columnas marcadas como searchable.

  • value: El texto ingresado por el usuario en el cuadro de búsqueda
  • regex: Booleano que indica si el value debe ser interpretado en forma literal o como expresión regular

columns: Arreglo con una entrada por cada columna de la tabla.

Cada entrada de este arreglo será, a su vez, un arreglo con las siguientes claves:

  • data: información de mapeo entre el arreglo de datos y la tabla HTML (En este ejemplo se trata del índice que vincula la columna en la visualización con el campo en la consulta: id => 0, name => 1, price => 2)
  • name: Nombre de la columna como está definido en la propiedad columns.name (En este ejemplo no lo estamos usando así que será un string vacío).
  • searchable: Booleano que indica si es posible realizar búsquedas sobre esta columna
  • orderable: Booleano que indica si es posible realizar ordenamientos basados en esta columna
  • search: Arreglo que contiene los parámetros de la búsqueda realizada por el usuario. Similar a la búsqueda global, sólo que aplicada a esta columna en particular (Un caso algo más avanzado y no muy frecuentemente utilizado).
    • value: El texto ingresado por el usuario en el cuadro de búsqueda
    • regex: Booleano que indica si el value debe ser interpretado en forma literal o como expresión regular

order: Arreglo que especifica el criterio de ordenamiento que debe aplicarse, contendrá una entrada por cada criterio a utilizar (Para el caso de que se quieran combinar varios). Dentro de cada entrada contendrá un arreglo con dos claves:

  1. column: Número de columna por la que se quiere ordenar
  2. dir: String con la dirección del ordenamiento (asc para ascendente, desc para descendiente)

Como te podrás imaginar, la URL de la llamada puede ser algo extensa 🙂

¿No me creés? ¿Qué te parece esta?

http://localhost:8000/get_data.php?draw=1&columns%5B0%5D%5Bdata%5D=0&columns%5B0%5D%5Bname%5D=&columns%5B0%5D%5Bsearchable%5D=true&columns%5B0%5D%5Borderable%5D=true&columns%5B0%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B0%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B1%5D%5Bdata%5D=1&columns%5B1%5D%5Bname%5D=&columns%5B1%5D%5Bsearchable%5D=true&columns%5B1%5D%5Borderable%5D=true&columns%5B1%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B1%5D%5Bsearch%5D%5Bregex%5D=false&columns%5B2%5D%5Bdata%5D=2&columns%5B2%5D%5Bname%5D=&columns%5B2%5D%5Bsearchable%5D=true&columns%5B2%5D%5Borderable%5D=true&columns%5B2%5D%5Bsearch%5D%5Bvalue%5D=&columns%5B2%5D%5Bsearch%5D%5Bregex%5D=false&order%5B0%5D%5Bcolumn%5D=0&order%5B0%5D%5Bdir%5D=asc&start=0&length=10&search%5Bvalue%5D=&search%5Bregex%5D=false&_=1619613946473

Si la pasás a través de urldecode queda un poquito más entendible:

http://localhost:8000/get_data.php?draw=1&columns[0][data]=0&columns[0][name]=&columns[0][searchable]=true&columns[0][orderable]=true&columns[0][search][value]=&columns[0][search][regex]=false&columns[1][data]=1&columns[1][name]=&columns[1][searchable]=true&columns[1][orderable]=true&columns[1][search][value]=&columns[1][search][regex]=false&columns[2][data]=2&columns[2][name]=&columns[2][searchable]=true&columns[2][orderable]=true&columns[2][search][value]=&columns[2][search][regex]=false&order[0][column]=0&order[0][dir]=asc&start=0&length=10&search[value]=&search[regex]=false&_=1619613946473

O, todavía mejor si lo ves desde la consola del navegador:

De modo que si del lado de tu PHP hacés un var_dump($_GET) te vas a encontrar con:

array (size=7)
  'draw' => string '1' (length=1)
  'columns' => 
    array (size=3)
      0 => 
        array (size=5)
          'data' => string '0' (length=1)
          'name' => string '' (length=0)
          'searchable' => string 'true' (length=4)
          'orderable' => string 'true' (length=4)
          'search' => 
            array (size=2)
              ...
      1 => 
        array (size=5)
          'data' => string '1' (length=1)
          'name' => string '' (length=0)
          'searchable' => string 'true' (length=4)
          'orderable' => string 'true' (length=4)
          'search' => 
            array (size=2)
              ...
      2 => 
        array (size=5)
          'data' => string '2' (length=1)
          'name' => string '' (length=0)
          'searchable' => string 'true' (length=4)
          'orderable' => string 'true' (length=4)
          'search' => 
            array (size=2)
              ...
  'order' => 
    array (size=1)
      0 => 
        array (size=2)
          'column' => string '0' (length=1)
          'dir' => string 'asc' (length=3)
  'start' => string '0' (length=1)
  'length' => string '10' (length=2)
  'search' => 
    array (size=2)
      'value' => string '' (length=0)
      'regex' => string 'false' (length=5)
  '_' => string '1619615674442' (length=13)

Y de ahí… bueno, habrá que hacer la consulta que corresponda, por ejemplo:

<?php

try {
    $conn = new PDO('sqlite:dt.sq3');
} catch (PDOException $exception) {
    die($exception->getMessage());
}

$orderBy = " ORDER BY ";
foreach ($_GET['order'] as $order) {
    $orderBy .= $order['column'] + 1 . " {$order['dir']}, ";
}

$orderBy = substr($orderBy, 0, -2);
$where = '';

$columns = $_GET['columns'];
$fields = ['id', 'name', 'price'];
$where = '';

foreach ($columns as $k => $column) {
    if ($search = $column['search']['value']) {
        $where .= $fields[$k].' = '.$search.' AND ';
    }
}

$where = substr($where, 0, -5);
$length = $_GET['length'];
$start = $_GET['start'];

$sql = "SELECT * FROM products ".($where ? "WHERE $where " : '')."$orderBy LIMIT $length OFFSET $start";

(Me tomé una pequeña licencia… asumí que el search no será una expresión regular… te queda como ejercicio :))

Cómo debe responder el BackEnd al FrontEnd en DataTables Server-Side

Por otro lado, es importante que tu php sepa qué es lo que DataTables está esperando como respuesta, de otro modo la comunicación no podrá realizarse exitosamente.

Lo principal: DataTables espera un resultado JSON que debe contener:

data: El arreglo de los registros que se levantaron de la base

recordsTotal: Número total de registros que existen en la base (Sin aplicar filtros)

recordsFiltered: Número de registros que devuelve la consulta (Una vez aplicados los filtros).

En definitiva, el código php se completa con:

$countSql = "SELECT count(id) as Total FROM products";
$countSt = $conn
    ->query($countSql);

$total = $countSt->fetch()['Total'];

$sql = "SELECT * FROM products ".($where ?? "WHERE $where ")."$orderBy LIMIT $length OFFSET $start";
$st = $conn
    ->query($sql);

if ($st) {
    $rs = $st->fetchAll(PDO::FETCH_FUNC, fn($id, $name, $price) => [$id, $name, $price] );

    echo json_encode([
        'data' => $rs,
        'recordsTotal' => $total,
        'recordsFiltered' => count($rs),
    ]);
} else {
    var_dump($conn->errorInfo());
    die;
}

El código completo lo podés descargar de aquí.

DataTables ServerSide FTW!

En este artículo aprendiste las razones por las que tu página con DataTables tarda mucho en cargar y cómo podés acelerar los tiempos de carga.

¿Siempre te conviene usar ServerSide? No

Si la cantidad de registros es pequeña (O más bien diría si no hay retrasos visibles) no te conviene hacer una implementación con esta complejidad, pero en cambio, si los DataTables se están volviendo un cuello de botella SeverSide puede ser una buena solución.

Ahora que lo sabés, andá a acelerar esa página que los usuarios están esperando!

mchojrin

Por mchojrin

Ayudo a desarrolladores PHP a afinar sus habilidades técnicas y avanzar en sus carreras

11 comentarios

  1. Hola, gracias por el post, te quería hacer una pregunta, como hacer para exportar los datos filtrados o todos los registros usando Server Side?, ya que solo exporta lo que ve en pantalla. Muchas Gracias!

  2. Buenas de nuevo. Además, observo que este script solo permite mostrar los campos seguidos uno detrás de otro. Si por ejemplo la tabla a mostrar tiene 20 campos y tan sólo quieres mostrar 5 campos pero no consecutivos, sino alternos, simplemente con esto no puedes, te mostrará solamente los 5 primeros campos.

    1. Hola Marcos:

      ¿Probaste hacer una consulta diferente de SELECT * FROM products? Ahí puedes elegir qué campos quieres utilizar y de qué tabla (o tablas) extrarerlos.

      Una vez hayas obtenido los datos en el backend será cuestión de armar la tabla que quieres mostrar en el frontend de modo que se ajuste a tus necesidades.

      Saludos,

  3. Muchas gacias por su aportación, pero tengo unda duda. Que sucede cuando la consulta SQL es combinada? en el ejemplo solo se conculta la tabla productos, pero como hacer cuando la consulta SQL es com varios JOIN ?

    1. Hola, esto yo lo solucione poniendo los Inner Join en la variable $tabla, por ejemplo:

      $table =’Tabla1 UNO INNER JOIN Tabla2 ON tabla2.campo= UNO.campo
      INNER JOIN Tabla3 ON tabla3.campo = tabla2.campo’

  4. hola Profesor! Estuve por tu curso de mysql en edteam juju
    Tengo el error. Invalid Json response. Desde el localhost desde mi maquina funcionaba correcto, incluso probe en otras maquinas con xampp y funciona, el problema se hizo cuando subi a un servidor. Apache mariadb. Lo que note es que en el phpmyadmin, el server charset esta en cp1252 West European (latin1)
    y en las demas esta UTF-8 Unicode (utf8mb4). Pero no es collation de la database. Es algo general del server esa config..

    1. Hola:

      Es posible que la diferencia en el character encoding esté afectando el json que se genera. Algo que podrías hacer es verificar a través de la consola de tu navegador, en la pestaña de red, a ver qué es exactamente lo que el servidor está enviando.

      Espero te ayude!

      Saludos,

    1. Hola Eduardo:

      Gracias por tu pregunta. Esa situación es algo más compleja y habría que analizarla en detalle para buscarle una solución.

      Básicamente se tratará de encontrar el cuello de botella primero. Lo más probable es que se trate de una consulta sub-óptima a la base de datos (Este es el escenario más común).

      Otra opción es que el procesamiento de los datos del lado del servidor (es decir, en tu php) sea ineficiente.

      Algo que también puede afectar es si hay muchos usuarios conectándose a la vez a la aplicación.

      Como te digo, es para investigar en mayor profundidad antes de definir una solución específica.

      Saludos,

  5. compa, me descargue el codigo que dejaste, pero no funciona, o no supe hacerlo funcionar, porque mi duda es la siguiente, de donde obtiene los datos que se envian GET? no entendi como funciona esa parte.

    1. Hola Biskukuy:

      Habría que ver exactamente qué mensaje de error estás recibiendo para comprender la causa del problema…

      Respecto de los datos que se envían no sé si comprendí bien tu duda. Los datos que se intercambian se envían a través de Ajax (Todo esto lo maneja la librería DataTable).

      Saludos!

¿Te quedó alguna duda? Publica aca tu pregunta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.